Search the source of every open source package.
385 results
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox
@*
The floating name tag above an avatar. StreamPlayer.Setup() points its Viewer at the right viewer,
and we render their display name in their chat colour.
*@
<root>
<div class="username" style="@NameStyle">
@Viewer.DisplayName
</div>
</root>
@code
{
/// <summary>
/// The viewer this tag is labelling. Set by StreamPlayer when the avatar is spawned.
/// </summary>
public Streamer.Viewer Viewer { get; set; }
/// <summary>
/// Colour the name with the viewer's chat colour, if they have one set.
/// </summary>
string NameStyle => Viewer.Color.HasValue ? $"color: {Viewer.Color.Value.Hex}" : null;
/// <summary>
/// PanelComponent rebuilds the markup whenever this hash changes - so include anything we display.
/// </summary>
protected override int BuildHash() => System.HashCode.Combine( Viewer.DisplayName, Viewer.Color );
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("DifficultyPanel.razor.scss")]
@{
var isClient = Networking.IsClient;
int diff = DontChangeGameDifficulty ? DifficultyToDisplay : Manager.Instance.Difficulty;
// var canDecreaseDifficulty = diff > Manager.MinDifficulty;
var isAtMinDifficulty = diff <= Manager.MinDifficulty - (DontChangeGameDifficulty ? 1 : 0);
var canIncreaseDifficulty = DontChangeGameDifficulty || ( diff < Manager.MaxDifficulty && Manager.Instance.HasBeatenDifficulty(diff) );
var isAtMaxDifficulty = diff >= Manager.MaxDifficulty;
var difficultyDesc = Manager.GetDescriptionForDifficulty( diff );
}
<root>
<div class="top-row">
@if( isAtMinDifficulty)
{
<div class="difficulty_decrease disabled_button" style="opacity:0;"> </div>
}
else
{
<div class="difficulty_decrease @(isClient && !DontChangeGameDifficulty ? "disabled_button" : "")" onclick="@(() => ButtonLeft())">
@if(Input.UsingController && (!isClient || DontChangeGameDifficulty)) { <InputHint class="inputbutton" Button="Slot1" /> }
</div>
}
<div class="middle">
<div class="difficulty_label" style="color:@(Manager.GetDifficultyLabelColor(diff).Rgba);">@($"{Manager.GetNameForDifficulty(diff)}")</div>
@if ( !string.IsNullOrEmpty( difficultyDesc ) )
{
<label class="description">@difficultyDesc</label>
}
</div>
@if(isAtMaxDifficulty)
{
<div class="difficulty_increase disabled_button" style="opacity:0;"></div>
}
else
{
if(canIncreaseDifficulty)
{
<div class="difficulty_increase @(isClient && !DontChangeGameDifficulty ? "disabled_button" : "")" onclick="@(() => ButtonRight())">
@if(Input.UsingController && (!isClient || DontChangeGameDifficulty)) { <InputHint class="inputbutton" Button="Slot3" /> }
</div>
}
else
{
<div class="difficulty_locked" Tooltip=@($"Beat {Manager.GetNameForDifficulty(diff)} first")></div>
}
}
</div>
</root>
@code
{
public bool DontChangeGameDifficulty { get; set; }
public static int DifficultyToDisplay { get; set; } = -1; // -1 equals all difficulties combined
public override void Tick()
{
base.Tick();
if(!Input.UsingController)
return;
if(Networking.IsClient && !DontChangeGameDifficulty)
return;
int diff = DontChangeGameDifficulty ? DifficultyToDisplay : Manager.Instance.Difficulty;
var isAtMinDifficulty = diff <= Manager.MinDifficulty - (DontChangeGameDifficulty ? 1 : 0);
var canIncreaseDifficulty = DontChangeGameDifficulty || ( diff < Manager.MaxDifficulty && Manager.Instance.HasBeatenDifficulty(diff) );
var isAtMaxDifficulty = diff >= Manager.MaxDifficulty;
if(Input.Pressed("Slot1") && !isAtMinDifficulty) { Manager.Instance.PlaySfxUI("click", pitch: 1.15f, volume: 0.75f); ButtonLeft(); }
else if(Input.Pressed("Slot3") && !isAtMaxDifficulty) { Manager.Instance.PlaySfxUI("click", pitch: 1.15f, volume: 0.75f); ButtonRight(); }
}
protected override int BuildHash()
{
return HashCode.Combine(
Manager.Instance.Difficulty,
DifficultyToDisplay,
Input.UsingController
);
}
void ButtonLeft()
{
GameSettingsSystem.Save();
ButtonLeftAsync();
}
async void ButtonLeftAsync()
{
if(DontChangeGameDifficulty)
{
if(DifficultyToDisplay == Manager.MinDifficulty)
DifficultyToDisplay = -1;
else
DifficultyToDisplay = Math.Max(DifficultyToDisplay - 1, Manager.MinDifficulty);
}
else
{
Manager.Instance.FadeRpc(fadeIn: false);
await Task.Frame();
var difficulty = Math.Max(Manager.Instance.Difficulty - 1, Manager.MinDifficulty);
Manager.Instance.SetDifficulty(difficulty);
}
}
void ButtonRight()
{
ButtonRightAsync();
GameSettingsSystem.Save();
}
async void ButtonRightAsync()
{
if(DontChangeGameDifficulty)
{
DifficultyToDisplay = Math.Min(DifficultyToDisplay + 1, Manager.MaxDifficulty);
}
else
{
Manager.Instance.FadeRpc(fadeIn: false);
await Task.Frame();
var difficulty = Math.Min(Manager.Instance.Difficulty + 1, Manager.MaxDifficulty);
Manager.Instance.SetDifficulty(difficulty);
}
}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("InfoPanel.razor.scss")]
<root>
@{
// todo: split each ui line into its own component
var xp_percent = Player.ExperienceCurrent / (float)Player.ExperienceRequired;
var xp_transition_enabled = Player.GetUiStat( PlayerStat.DisableXpBarTransition ) == 0f;
var hp_percent = Math.Clamp(Math.Max(Player.Health, 0f) / Player.GetSyncStat(PlayerStat.MaxHp), 0f, 1f);
// var hp_regen = Player.Stats[PlayerStat.HealthRegen] + (Player.IsMoving ? 0f : Player.Stats[PlayerStat.HealthRegenStill]);
var hp_regen = Player.GetHpRegenAmount(forDisplay: true);
var hpBarHidden = Player.GetUiStat( PlayerStat.HpBarHidden ) > 0f;
}
<div class="info_bar">
<div class="bar_container" style=@("mask-image: url(\"/textures/ui/panel/info_panel_xp_mask.png\");")>
<div class="info_bar_overlay @(xp_transition_enabled ? "xp_transition_on" : "")" style="width:@(xp_percent * 100f)%; background-color: #8888ff77;"></div>
@if(Manager.Instance.Difficulty >= Manager.Instance.FirstDifficultyWithCurses && Player.AvailableCurseCount > 0)
{
var cursePercent = Player.IsBeingShownCurseChoices ? 1f : Utils.Map(Player.CurrLevelsUntilCurseChoice, Player.NumLevelsBetweenCurseChoices, 0, 0f, 1f);
var colorSpeed = Player.IsBeingShownCurseChoices ? 15f : Utils.Map(Player.CurrLevelsUntilCurseChoice, Player.NumLevelsBetweenCurseChoices, 0, 3f, 15f, EasingType.SineIn);
var curseColor = Color.Lerp(new Color(0.2f, 0.1f, 0.4f, 0.5f), new Color(0.8f, 0.1f, 0.9f, 0.25f), Utils.Map(Utils.FastSin(RealTime.Now * colorSpeed), -1f, 1f, 0f, 1f));
<div class="info_bar_overlay" style="width:@(cursePercent * 100f)%; background-color: @curseColor.Rgba;"></div>
}
</div>
<div class="info_bar_label">XP</div>
<div class="data_container">
<div class="data" style="opacity: 0.3;">@($"LVL {Player.Level}")</div>
</div>
</div>
@{
var regularMaxDashes = (int)MathF.Round( Player.GetUiStat( PlayerStat.NumDashes ) );
var maxDashes = regularMaxDashes + Player.NumTempDashesAvailable;
var totalAvailable = Player.NumDashesAvailable + Player.NumTempDashesAvailable;
}
@if( maxDashes < 9 )
{
<div class="info_dash_container" style="gap: @(Utils.Map(maxDashes, 1, 8, 8, 4, EasingType.QuadIn))px;">
<div class="dash_bar_container")>
@for(int i = 0; i < maxDashes; i++)
{
// Bars i < regularMaxDashes are regular dashes; i >= regularMaxDashes are temporary (from PerkDashTemporary).
// Temp bars are always shown to the right and colored purple.
var isTemp = i >= regularMaxDashes;
float progress;
bool isRecharging;
if ( isTemp )
{
// Temp dashes are either fully available or gone — no partial recharge animation.
int tempIndex = i - regularMaxDashes;
progress = tempIndex < Player.NumTempDashesAvailable ? 1f : 0f;
isRecharging = false;
}
else
{
// The bar at NumDashesAvailable is the one currently recharging (partial fill).
// Bars below it are full; bars above it are empty.
progress = i == Player.NumDashesAvailable ? Player.DashRechargeProgress : (i < Player.NumDashesAvailable ? 1f : 0f);
isRecharging = i >= Player.NumDashesAvailable;
}
var filledColor = isTemp ? "#b34dff88" : "#00ff0088";
var rechargingColor = isTemp ? "#b34dff44" : "#00ff4444";
<div class="info_bar" style="background-color: #00000077;">
<div class="info_bar_overlay" style="width:@(progress * 100f)%; background-color: @(isRecharging ? rechargingColor : filledColor);"></div>
</div>
}
</div>
@if(maxDashes <= 0)
{
<div class="info_bar">
</div>
}
<div class="info_bar_label" style="left: 8px;">
@("DASH")
</div>
</div>
}
else
{
var dashPercent = (float)totalAvailable / (float)maxDashes;
<div class="info_bar">
<div class="info_bar_overlay" style="width:@(dashPercent * 100f)%; background-color: #00ff0055; transition: all 0.2s ease-in-out;"></div>
<div class="info_bar_label">DASH</div>
<div class="data_container">
<div class="data">@maxDashes</div>
<div class="data">/</div>
<div class="data">@totalAvailable</div>
</div>
</div>
}
<div class="info_bar">
<div class="bar_container" style=@("mask-image: url(\"/textures/ui/panel/info_panel_hp_mask.png\");")>
@if ( hpBarHidden )
{
<div class="info_bar_overlay" style="width: 100%; background-color: #7a0040ff;"></div>
}
else
{
<div class="info_bar_overlay" style="width:@(hp_percent * 100f)%; background-color: #ffffffff; transition: all 0.4s ease-in-out "></div>
<div class="info_bar_overlay" style="width:@(hp_percent * 100f)%; background-color: #ff0000ff; transition: all 0.2s ease-in-out;"></div>
}
</div>
<div class="info_bar_label">HP</div>
<div class="data_container">
<div class="data">@($"{(int)MathF.Round(Player.GetSyncStat(PlayerStat.MaxHp))}")</div>
<div class="data">/</div>
@if ( hpBarHidden )
{
<div class="data">?</div>
}
else
{
<div class="data">@($"{(Math.Max(Player.Health, 0f) > 0f && Math.Max(Player.Health, 0f) < 1f ? (int)Math.Ceiling(Math.Max(Player.Health, 0f)) : (int)Math.Round(Math.Max(Player.Health, 0f)))}")</div>
@if (Math.Abs(hp_regen) > 0f)
{
<div class="data" style="color:@((hp_regen > 0f ? new Color(0f, 1f, 0f) : new Color(1f, 0f, 0f)).Rgba); letter-spacing: 2px; padding-right: 10px;">@($"{(hp_regen > 0f ? "+" : "")}{hp_regen.ToString("0.##")}")</div>
}
}
</div>
</div>
@if (Player.Armor > 0)
{
<div class="armor_icon" style="transform: scale(@(Utils.Map(Player.RealTimeSinceArmorChanged, 0f, 0.5f, 1.35f, 1f, EasingType.QuadOut)));">
<label class="armor_text">@($"{MathX.CeilToInt(Player.Armor)}")</label>
</div>
}
</root>
@code
{
public Player Player { get; set; }
protected override int BuildHash()
{
if( Manager.Instance.Difficulty >= Manager.Instance.FirstDifficultyWithCurses )
{
return HashCode.Combine(RealTime.Now);
}
var hpBarHidden = Player.GetUiStat( PlayerStat.HpBarHidden ) > 0f;
var hpHash = HashCode.Combine(
Player.Health,
Player.GetSyncStat(PlayerStat.MaxHp),
Player.GetSyncStat( PlayerStat.HealthRegen ),
Player.GetSyncStat( PlayerStat.HealthRegenStill ),
Player.IsMoving,
Player.GetHpRegenAmount( forDisplay: true ),
hpBarHidden
);
var armorHash = HashCode.Combine(
Player.RealTimeSinceArmorChanged > 0.5f ? 0f : Player.RealTimeSinceArmorChanged.Relative,
Player.Armor
);
var xpHash = HashCode.Combine(
Player.Level,
Player.ExperienceCurrent,
Player.ExperienceRequired
);
return HashCode.Combine(
Player.DashRechargeProgress,
Player.NumDashesAvailable,
Player.NumTempDashesAvailable,
Player.GetUiStat( PlayerStat.NumDashes ),
hpHash,
armorHash,
xpHash
);
}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Linq;
@using System.Collections.Generic;
@inherits Panel
@attribute [StyleSheet("LoadoutPanel.razor.scss")]
<root>
<div class="hide_button" onclick=@(() => Close())></div>
<div class="title_label">Loadout</div>
<div class="tabs">
@for(int t = 0; t < TabNames.Length; t++)
{
var tabIndex = t;
<div class="tab @(_selectedTab == tabIndex ? "active" : "")" onclick=@(() => SelectTab(tabIndex))>
@TabNames[tabIndex]
</div>
}
</div>
<div class="panel_body">
@if(_selectedTab == 2)
{
var equippedGems = ProgressManager.GetEquippedGems();
var ownedGems = ProgressManager.GetOwnedItemsByCategory(ShopItemCategory.Gem);
var maxGemSlots = ProgressManager.GetSelectedGunSocketCount();
var gemsFull = equippedGems.Count >= maxGemSlots;
@if(ownedGems.Count > 0)
{
<div class="items_grid">
@foreach(var gem in ownedGems)
{
var isEquipped = equippedGems.Contains(gem.Id);
var gemCapture = gem;
<LoadoutItemPanel Item=@gem IsSelected=@isEquipped IsSlotFull=@(!isEquipped && gemsFull) OnClick=@(() => OnSelectGem(gemCapture)) />
}
</div>
}
else
{
<div class="coming_soon">Buy gems in the Shop to equip them here.</div>
}
}
else
{
var items = GetItemsForTab(_selectedTab);
@if(_selectedTab == 1)
{
var selectedCharmIds = ProgressManager.GetSelectedCharmIds();
var maxCharmSlots = ProgressManager.GetSelectedGunCharmSlotCount();
var charmsFull = selectedCharmIds.Count >= maxCharmSlots;
<div class="items_grid">
@foreach(var item in items)
{
var isSelected = selectedCharmIds.Contains(item.Id);
var itemCapture = item;
<LoadoutItemPanel Item=@item IsSelected=@isSelected IsSlotFull=@(!isSelected && charmsFull && maxCharmSlots > 1) OnClick=@(() => OnSelectItem(itemCapture)) />
}
</div>
}
else
{
var selectedGunId = ProgressManager.GetSelectedGunId();
<div class="items_grid">
@foreach(var item in items)
{
var isSelected = item.Id == selectedGunId;
var itemCapture = item;
<LoadoutItemPanel Item=@item IsSelected=@isSelected OnClick=@(() => OnSelectItem(itemCapture)) />
}
</div>
}
}
</div>
</root>
@code
{
static readonly string[] TabNames = { "Guns", "Charms", "Gems" };
static readonly ShopItemCategory[] TabCategories = { ShopItemCategory.Gun, ShopItemCategory.Charm, ShopItemCategory.Gem };
static int _selectedTab = 0;
static bool _initialTabSet = false;
protected override void OnAfterTreeRender(bool firstTime)
{
base.OnAfterTreeRender(firstTime);
if(firstTime)
{
GunPreviewController.Show();
if(!_initialTabSet)
{
_selectedTab = BestStartingTab();
_initialTabSet = true;
}
}
}
int BestStartingTab()
{
for(int i = 0; i < TabCategories.Length; i++)
if(GetItemsForTab(i).Count > 0)
return i;
return 0;
}
List<ShopItemDef> GetItemsForTab(int tab)
{
if(tab < 0 || tab >= TabCategories.Length)
return new List<ShopItemDef>();
return ProgressManager.GetOwnedItemsByCategory(TabCategories[tab]);
}
void OnSelectItem(ShopItemDef item)
{
if(_selectedTab == 0)
{
var currentGunId = ProgressManager.GetSelectedGunId();
var deselecting = currentGunId == item.Id;
var targetDef = deselecting ? ProgressManager.DefaultGun : item;
// Unequip gems that exceed the target gun's socket count
var newSocketCount = targetDef.GemSocketCount > 0 ? targetDef.GemSocketCount : 3;
var equippedGems = ProgressManager.GetEquippedGems();
while(equippedGems.Count > newSocketCount)
{
ProgressManager.UnequipGem(equippedGems[equippedGems.Count - 1]);
equippedGems = ProgressManager.GetEquippedGems();
}
// Trim selected charms to the target gun's charm slot count
var newCharmSlots = targetDef.CharmSlotCount > 0 ? targetDef.CharmSlotCount : 1;
var selectedCharms = ProgressManager.GetSelectedCharmIds();
if(selectedCharms.Count > newCharmSlots)
ProgressManager.SetSelectedCharmIds(selectedCharms.Take(newCharmSlots).ToList());
ProgressManager.SetSelectedGunId(deselecting ? ProgressManager.DefaultGun.Id : item.Id);
// Broadcast gun swap so all clients see the new model
var player = Manager.Instance.LocalPlayer;
if(player.IsValid())
{
var gunPrefab = ProgressManager.GetPrefabPath(targetDef.Id) ?? "prefabs/guns/gun_default.prefab";
var charmIds = ProgressManager.GetSelectedCharmIds();
var charm0 = charmIds.Count > 0 ? ProgressManager.GetPrefabPath(charmIds[0]) ?? "" : "";
var charm1 = charmIds.Count > 1 ? ProgressManager.GetPrefabPath(charmIds[1]) ?? "" : "";
var (g0, g1, g2, g3) = GetEquippedGemPrefabs();
player.SwapGunRpc(gunPrefab, charm0, g0, g1, g2, g3, charm1);
}
GunPreviewController.Refresh();
}
else if(_selectedTab == 1)
{
var selectedIds = ProgressManager.GetSelectedCharmIds();
var maxSlots = ProgressManager.GetSelectedGunCharmSlotCount();
if(selectedIds.Contains(item.Id))
{
// Deselect
ProgressManager.SetSelectedCharmIds(selectedIds.Where(id => id != item.Id).ToList());
}
else if(maxSlots == 1)
{
// Single-slot gun: replace
ProgressManager.SetSelectedCharmIds(new List<string> { item.Id });
}
else if(selectedIds.Count < maxSlots)
{
// Multi-slot gun with a free slot: add
ProgressManager.SetSelectedCharmIds(selectedIds.Concat(new[] { item.Id }).ToList());
}
else
{
// All slots full: do nothing, player must deselect first
return;
}
// Broadcast charm swap so all clients see the new model
var player = Manager.Instance.LocalPlayer;
if(player.IsValid())
{
var ids = ProgressManager.GetSelectedCharmIds();
var charm0 = ids.Count > 0 ? ProgressManager.GetPrefabPath(ids[0]) ?? "" : "";
var charm1 = ids.Count > 1 ? ProgressManager.GetPrefabPath(ids[1]) ?? "" : "";
player.SwapCharmRpc(charm0, charm1);
}
GunPreviewController.Refresh();
}
StateHasChanged();
}
void OnSelectGem(ShopItemDef gem)
{
var equipped = ProgressManager.GetEquippedGems();
var maxSlots = ProgressManager.GetSelectedGunSocketCount();
if(equipped.Contains(gem.Id))
ProgressManager.UnequipGem(gem.Id);
else if(equipped.Count < maxSlots)
ProgressManager.EquipGem(gem.Id, maxSlots);
BroadcastGemSwap();
GunPreviewController.Refresh();
StateHasChanged();
}
void BroadcastGemSwap()
{
var player = Manager.Instance.LocalPlayer;
if(!player.IsValid()) return;
var (g0, g1, g2, g3) = GetEquippedGemPrefabs();
player.SwapGemsRpc(g0, g1, g2, g3);
}
static (string, string, string, string) GetEquippedGemPrefabs()
{
var equipped = ProgressManager.GetEquippedGems();
string Get(int i) => i < equipped.Count ? (ProgressManager.GetPrefabPath(equipped[i]) ?? "") : "";
return (Get(0), Get(1), Get(2), Get(3));
}
void SelectTab(int tabIndex)
{
if(_selectedTab == tabIndex) return;
_selectedTab = tabIndex;
StateHasChanged();
}
void Close()
{
GunPreviewController.Hide();
Manager.Instance.ShowLoadoutPanel = false;
}
protected override int BuildHash()
{
return System.HashCode.Combine(
Manager.Instance.ShowLoadoutPanel,
_selectedTab,
ProgressManager.StateVersion
);
}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Text.Json;
@inherits Panel
@attribute [StyleSheet("LobbyNametagPanel.razor.scss")]
<root>
<LeaderboardPlayerIcon style="width: 42px; height: 42px;" PlayerInfo=@PlayerInfo />
<div class="name">@PlayerInfo.displayName</div>
@if(Networking.IsHost || PlayerInfo.steamId == Game.SteamId)
{
<i class="remove-btn" onclick=@KickPlayer>disabled_by_default</i>
}
</root>
@code
{
const int MAX_NAME_LENGTH = 13;
public PlayerInfo PlayerInfo { get; set; }
void KickPlayer()
{
Manager.Instance?.RequestKickPlayer(PlayerInfo.steamId);
}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("PlayerPerks.razor.scss")]
@{
var player = Manager.Instance.SelectedPlayer.IsValid()
? Manager.Instance.SelectedPlayer
: Manager.Instance.LocalPlayer;
if (!player.IsValid())
return;
var height = Manager.Instance.Players.Count > 1 ? 880f : 950f;
}
<root style="height: @(height)px; max-height: @(height)px;">
@if( player.IsProxy )
{
if (player.SyncPerks.Count == 0)
return;
int index = 0;
<div class="itemlist" style="opacity:@(1f);">
@foreach (var pair in player.SyncPerks)
{
var rot = Utils.Map((player.PerkRandomRotationSeed + index * 3f) % 10, 0, 9, -5f, 5f);
<PerkIconStatic style="width: 50px; height: 50px; transform: rotate(@(rot)deg);" PerkType=@(PerkManager.IdentityToType(pair.Key)) Level=@(pair.Value) ProgressLevel=@(pair.Value) Banished=@false />
index++;
}
</div>
}
else
{
if (player.Perks.Count == 0)
return;
int index = 0;
<div class="itemlist @(Manager.Instance.IsGameOver ? "" : "itemlist_hover")">
@foreach (var (_, perk) in player.Perks)
{
var rot = Utils.Map((player.PerkRandomRotationSeed + index * 3f) % 10, 0, 9, -5f, 5f);
<PerkIcon style="width: 50px; height: 50px;" Perk=@perk Angle=@rot Banished=@false />
index++;
}
</div>
}
</root>
@code
{
protected override int BuildHash()
{
var player = Manager.Instance.SelectedPlayer.IsValid()
? Manager.Instance.SelectedPlayer
: Manager.Instance.LocalPlayer;
var perkHash = player.IsValid()
? player.PerkHash
: 0;
var perkCount = player.IsValid()
? player.SyncPerks.Count
: 0;
return HashCode.Combine(
player,
// player.Id // todo: this instead?
perkCount,
perkHash,
Manager.Instance.IsGameOver
// Time.Now
);
}
}
@using Sandbox;
@using Sandbox.UI;
@using System.Threading.Tasks;
@using System.Collections.Generic;
@using System;
@inherits PanelComponent
<root class="@(IsFadingOut ? "fade-out" : "fade-in")" style="background-image: @(!string.IsNullOrEmpty(BackgroundImage) ? $"url({BackgroundImage})" : "none");">
<div class="content">
@* Logo Image (.png / .jpg) *@
@if (!string.IsNullOrEmpty(LogoImage))
{
<img class="logo" src="@LogoImage" />
}
@* Text Lines *@
@if (TextLines != null && TextLines.Count > 0)
{
<div class="text-container">
@foreach (var line in TextLines)
{
<label style="color: @line.TextColor.Hex; font-size: @(line.FontSize)px;">
@line.Text
</label>
}
</div>
}
</div>
</root>
@code {
// === CUSTOM DATA CLASS FOR TEXT LINES ===
public class SplashTextLine
{
[Property, Description("The text to display.")]
public string Text { get; set; } = "NEW LINE";
[Property, Description("Text color for this specific line.")]
public Color TextColor { get; set; } = Color.White;
[Property, Description("Font size for this specific line.")]
public float FontSize { get; set; } = 80f;
}
// === IMAGE SETTINGS ===
[Property, ImageAssetPath, Group("Images"), Description("Supports .png and .jpg. If empty, the background will be black.")]
public string BackgroundImage { get; set; }
[Property, ImageAssetPath, Group("Images"), Description("Main logo image (.png / .jpg). Appears above the text if both are set.")]
public string LogoImage { get; set; }
// === TEXT SETTINGS ===
[Property, Group("Text"), Description("Add text lines with individual settings (color, size).")]
public List<SplashTextLine> TextLines { get; set; } = new();
// === AUDIO & SCENE SETTINGS ===
[Property, Group("Audio"), Description("Select a Sound Event (.sound) that contains your .mp3 or .ogg file.")]
public SoundEvent SplashSound { get; set; }
[Property, Group("Scene"), Description("The scene to load after the splash screen finishes.")]
public SceneFile NextScene { get; set; }
// === LOGIC ===
public bool IsFadingOut { get; set; } = false;
protected override void OnStart()
{
base.OnStart();
// Start the asynchronous sequence
_ = RunSplashSequence();
}
private async Task RunSplashSequence()
{
// 1. Wait half a second before starting to avoid stuttering during load
await Task.Delay(500);
// 2. Play the assigned sound (.mp3 / .ogg via Sound Event)
if (SplashSound != null)
{
Sound.Play(SplashSound);
}
// 3. Wait while the logo/text is visible on the screen (3 seconds)
await Task.Delay(3000);
// 4. Trigger the fade-out animation
IsFadingOut = true;
StateHasChanged(); // Notify the UI to update CSS classes
// 5. Wait for the fade-out animation to finish (matches the CSS transition time)
await Task.Delay(2000);
// 6. Load the next scene
if (NextScene != null)
{
Scene.Load(NextScene);
}
else
{
Log.Warning("Next Scene is not assigned in the Splash Screen component!");
GameObject.Destroy(); // Destroy the component if no scene is assigned
}
}
}@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
<root>
@if (Header != null)
{
<div class="header">@Header</div>
}
<div class="body menuinner">@Body</div>
</root>
@code
{
[Parameter] public RenderFragment Header { get; set; }
[Parameter] public RenderFragment Body { get; set; }
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
<root>
@Path
</root>
@code
{
public string Path { get; set; }
}
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Mounting;
@inherits Panel
@namespace Sandbox
<root>
<Button class="menu-action primary wide" Icon="➕" Text="#spawnmenu.spawnlist.new_button" Disabled=@( !CanCreate() ) @onclick=@CreatePopup></Button>
</root>
@using Sandbox;
@using Sandbox.UI;
@namespace Sandbox
@inherits Panel
<root>
<div class="menu-segmented-group">
@{
var activeMode = SpawnMenuHost.GetActiveMode();
foreach ( var mode in Game.TypeLibrary.GetTypesWithAttribute<SpawnMenuHost.SpawnMenuMode>().OrderBy( x => x.Type.Order ) )
{
if ( !mode.Attribute.CheckCondition() ) continue;
var activeClass = mode.Type.TargetType == activeMode?.GetType() ? "active" : "";
<div class="menu-mode-button @activeClass" @onclick="@( () => SpawnMenuHost.SwitchMode( mode.Type.Name ) )">
<div class="icon">@mode.Type.Icon</div>
<div class="title">@mode.Type.Title</div>
</div>
}
}
</div>
</root>
@code
{
protected override int BuildHash() => HashCode.Combine( SpawnMenuHost.GetActiveMode(), Game.TypeLibrary.GetTypesWithAttribute<SpawnMenuHost.SpawnMenuMode>() );
}
@using System
@using Sandbox
@using Sandbox.UI
@namespace Breakout
@inherits Panel
@if ( Game.IsValid() && Game.State != GameState.Menu && Game.Level <= 1 )
{
<root>
<div class="container">
<span class="controls-label">CONTROLS</span>
<div class="column">
<div class="input">
@*There is no mouse/ controller move input hint so we do this.*@
@if(Input.UsingController)
{
<label class="type">Move</label><img src="ui/glyphs/xbox/leftjoystickbutton.svg" class="mouse-glyph" />
}
else
{
<label class="type">Move Paddle</label><img src="ui/glyphs/default/mouse.svg" class="mouse-glyph" />
}
</div>
<div class="input">
<label class="type">Launch</label><InputHint Action="attack1" class="key-glyph" Dark=@true />
</div>
</div>
</div>
</root>
}
@code
{
/// <summary>
/// The game this bar reads score, lives, level and mode from.
/// </summary>
[Parameter] public BreakoutGame Game { get; set; }
// Update the ui when the game state changes or when the input method changes.
protected override int BuildHash() => System.HashCode.Combine(Game?.State, Game?.Mode, Game?.Level, Input.UsingController);
}
@using System
@using Sandbox
@using Sandbox.UI
@namespace Breakout
@inherits Panel
@*
HelpScreen is the "how to play" page you reach from the menu's HELP button. It shows a guide
of every block type and what it does. Pressing BACK runs the Back callback, which tells the
menu to show itself again.
*@
<root>
<div class="help">
<button class="back-btn" onclick=@(() => Back?.Invoke())>
<span class="back-icon">◀</span>
BACK
</button>
<div class="help-body">
<div class="help-title">HELP</div>
<div class="help-sub">Here is what each brick does when you hit it.</div>
<div class="block-grid">
@foreach ( var block in Blocks )
{
<div class="block-card">
<img class="block-icon" src="@block.Icon" />
<div class="block-info">
<div class="block-name">@block.Name</div>
<div class="block-desc">@block.Description</div>
</div>
</div>
}
</div>
</div>
</div>
</root>
@code
{
/// <summary>
/// Called when the player presses BACK, so the menu can show itself again.
/// </summary>
[Parameter] public Action Back { get; set; }
/// <summary>
/// One row in the block guide: the icon to show, the block's name, and a short description.
/// </summary>
private record BlockGuideEntry( string Icon, string Name, string Description );
// The list shown on the help page. Add a new line here to document another block type.
private static readonly BlockGuideEntry[] Blocks =
{
new( "textures/blocks/normal.png", "Brick", "Breaks in a single hit." ),
new( "textures/blocks/armoured.png", "Armoured", "Takes two hits to break." ),
new( "textures/blocks/unbreakable.png", "Unbreakable", "Can't be broken, so play around it." ),
new( "textures/blocks/explosion.png", "Explosive", "Blows up nearby bricks when it breaks." ),
new( "textures/blocks/speed.png", "Fast Ball", "Turns your ball into a faster one." ),
new( "textures/blocks/big_ball.png", "Big Ball", "Makes your ball big enough to smash armour in one hit." ),
new( "textures/blocks/fire_ball.png", "Fireball", "Turns your ball into a fireball that burns straight through bricks." ),
new( "textures/blocks/split.png", "Multi-Ball", "Drops a pickup that splits your ball into more." ),
new( "textures/blocks/expand.png", "Expand", "Drops a pickup that makes your paddle wider." ),
};
}
@namespace Sunless.Libraries.UI
@using Sandbox
@using Sandbox.UI
@attribute [StyleSheet]
@inherits Panel
<root class="modal-overlay @(IsOpen ? "is-open" : "") @ExtraClass">
@if ( IsOpen )
{
<div class="modal-scrim" @onclick=@HandleScrimClick></div>
<div class="modal-dialog" style=@DialogStyle>
@if ( Header is not null || !string.IsNullOrEmpty( Title ) || ShowClose )
{
<div class="modal-header">
@if ( Header is not null )
{
@Header
}
else
{
<div class="modal-title">@Title</div>
}
@if ( ShowClose )
{
<div class="modal-close" @onclick=@HandleClose>X</div>
}
</div>
}
@if ( Body is not null )
{
<div class="modal-body">@Body</div>
}
@if ( Footer is not null )
{
<div class="modal-footer">@Footer</div>
}
</div>
}
</root>
@code
{
public bool IsOpen { get; set; }
public string Title { get; set; }
public bool ShowClose { get; set; } = true;
public bool DismissOnScrim { get; set; } = true;
public string ExtraClass { get; set; }
public string DialogStyle { get; set; }
public Action OnClose { get; set; }
public RenderFragment Header { get; set; }
public RenderFragment Body { get; set; }
public RenderFragment Footer { get; set; }
void HandleClose()
{
try { OnClose?.Invoke(); } catch ( Exception e ) { Log.Warning( $"[Modal] OnClose threw: {e.Message}" ); }
}
void HandleScrimClick()
{
if ( !DismissOnScrim ) return;
HandleClose();
}
protected override int BuildHash() => HashCode.Combine( IsOpen, Title, ShowClose, ExtraClass, DialogStyle );
}
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.UI.Navigation;
@using Machines.Components;
@using Machines.Resources;
@namespace Machines.UI
@inherits Panel
@{
var flow = LobbyFlow.Current;
var map = flow?.SelectedMap;
var total = Connection.All.Count;
var ready = ReadyCount();
}
<root class="lobby-status @(Visible ? "" : "hidden") @(Input.UsingController ? "controller" : "mouse")" onclick=@GoToPlay>
@if ( Visible )
{
<div class="ls-map">
<div class="ls-thumb-wrap">
<image @ref="ThumbImage" class="ls-thumb" />
@if ( map?.Thumbnail == null )
{
<div class="ls-thumb-empty">NO IMAGE</div>
}
</div>
<div class="ls-map-info">
<span class="ls-eyebrow">IN LOBBY</span>
<span class="ls-title">@(map?.Title ?? "No map")</span>
</div>
</div>
<PlayerSlots class="ls-slots" />
<div class="ls-status">
<span class="ls-state">@StateLabel( flow )</span>
<span class="ls-ready">@ready / @System.Math.Max( total, 1 ) READY</span>
</div>
}
</root>
@code
{
public Sandbox.UI.Image ThumbImage { get; set; }
// Show when a lobby exists, except on /play which shows the full picker.
private bool Visible =>
LobbyFlow.Current is not null
&& MainMenuPanel.Instance?.CurrentUrl != "/play";
public override void Tick()
{
base.Tick();
if ( ThumbImage != null )
ThumbImage.Texture = LobbyFlow.Current?.SelectedMap?.Thumbnail;
StackAbovePatchNotes();
}
// Sit above the patch-notes card; explicit px so the CSS transition works both ways.
private void StackAbovePatchNotes()
{
var revision = FindRootPanel()?.Descendants.OfType<RevisionCard>().FirstOrDefault();
var cardHeight = revision is not null && revision.IsVisible
? revision.Box.Rect.Height * ScaleFromScreen + 14f
: 0f;
// Nudge up on the main menu home page so it clears the home layout.
var menuOffset = MainMenuPanel.Instance?.CurrentUrl == "/home" ? 32f : 0f;
Style.Bottom = Length.Pixels( 56f + cardHeight + menuOffset ); // $deadzone-y (+ card + gap)
}
// Click navigates back to the map selector.
private void GoToPlay()
{
this.Navigate( "/play" );
}
private static string StateLabel( LobbyFlow flow )
{
if ( flow == null )
return "WAITING";
return flow.State switch
{
LobbyState.Countdown => $"STARTING IN {System.Math.Max( 0, (int)System.Math.Ceiling( (float)flow.CountdownEnds ) )}…",
LobbyState.Launching => "STARTING…",
_ => "WAITING"
};
}
private static int ReadyCount()
{
var flow = LobbyFlow.Current;
if ( flow == null )
return 0;
return Connection.All.Count( c => flow.Ready.TryGetValue( c.Id, out var r ) && r );
}
protected override int BuildHash()
{
var flow = LobbyFlow.Current;
var countdownTick = flow?.State == LobbyState.Countdown ? (int)((float)flow.CountdownEnds * 4) : 0;
return System.HashCode.Combine(
Visible,
flow?.SelectedMapIdent,
flow?.State,
Connection.All.Count,
ReadyCount(),
countdownTick,
Input.UsingController );
}
}
@using Sandbox;
@using Sandbox.UI;
@using Machines.Components;
@using Machines.Race;
@using System;
@using System.Collections.Generic;
@namespace Machines.UI
@inherits Panel
@{
EnsureBaked();
var hasMap = _texture != null && _maxExtent > 0.001f && _trackRadius > 0.001f;
var center = new Vector2( 0.5f, 0.5f ); // whole track, centred on the map
var fitRadius = PanelSize * 0.5f - RimMargin; // fit the whole track inside the round clip
var disp = _maxExtent * (fitRadius / _trackRadius); // displayed track-image size in logical px
}
<root class="minimap @(hasMap ? "" : "empty")">
<div class="clip">
@if ( hasMap )
{
var imgLeft = PanelSize * 0.5f - center.x * disp;
var imgTop = PanelSize * 0.5f - center.y * disp;
<image @ref="MapImage" class="map-img"
style="width: @(disp)px; height: @(disp)px; left: @(imgLeft)px; top: @(imgTop)px;" />
@foreach ( var m in GetCheckpointMarkers( center, disp ) )
{
<div class="cp-bar @(m.IsFinish ? "finish" : "")"
style="left: @(m.X)px; top: @(m.Y)px; width: @(m.LengthPx)px; transform: translate(-50%, -50%) rotate(@(m.AngleDeg)deg);"></div>
}
@foreach ( var dot in GetBlipDots( center, disp ) )
{
var c = dot.Color;
var rgb = $"rgb({(int)(c.r * 255)}, {(int)(c.g * 255)}, {(int)(c.b * 255)})";
<div class="dot @dot.Class"
style="left: @(dot.X)px; top: @(dot.Y)px; background-color: @rgb;"></div>
}
}
</div>
</root>
@code
{
/// <summary>
/// Diameter of the minimap in logical px.
/// </summary>
public float PanelSize { get; set; } = 320;
/// <summary>
/// Inset (logical px) from the minimap edge that the track is fit within.
/// </summary>
private const float RimMargin = 32f;
/// <summary>
/// Resolution of the baked track texture (square).
/// </summary>
public int TextureResolution { get; set; } = 1024;
/// <summary>
/// Half-width of the drawn road in world units; tune to match the track mesh.
/// </summary>
public float RoadHalfWidth { get; set; } = 80f;
/// <summary>
/// Outline thickness around the road, in world units.
/// </summary>
public float OutlineWorld { get; set; } = 4f;
private Image MapImage { get; set; }
private Texture _texture;
private RacingLine _bakedFor; // rebake when this changes
private Vector2 _center; // world-XY centre of the baked region
private float _maxExtent; // world units covered by the baked region
private float _trackRadius; // world units from centre to outermost road edge
public override void Tick()
{
base.Tick();
if ( MapImage != null && _texture != null )
MapImage.Texture = _texture;
}
/// <summary>
/// Bakes the track texture once the racing line is available, or rebakes on change.
/// </summary>
private void EnsureBaked()
{
var line = RacingPath.Current?.Optimal;
if ( line is null || !line.IsValid )
return;
if ( _texture != null && _bakedFor == line )
return;
Bake( line, RoadHalfWidth );
_bakedFor = line;
}
private void Bake( RacingLine line, float halfWidth )
{
var res = TextureResolution;
// AA feather sized to ~1 displayed pixel for a smooth outline.
var (_, _, trackRadius) = TrackMapBaker.ComputeBounds( line, halfWidth, OutlineWorld );
var pxPerWorld = (PanelSize * 0.5f - RimMargin) / trackRadius;
var aaW = pxPerWorld > 0.0001f ? 1f / pxPerWorld : 0f;
var bake = TrackMapBaker.Bake( line, halfWidth, OutlineWorld, res, aaW );
if ( bake is null )
return;
_center = bake.Center;
_maxExtent = bake.MaxExtent;
_trackRadius = bake.TrackRadius;
_texture = Texture.Create( res, res )
.WithFormat( ImageFormat.RGBA8888 )
.WithData( bake.Rgba )
.Finish();
}
private struct CpMarker
{
public float X;
public float Y;
public float AngleDeg;
public float LengthPx;
public bool IsFinish;
}
/// <summary>
/// Bar across the track at each checkpoint, oriented to the line tangent.
/// </summary>
private List<CpMarker> GetCheckpointMarkers( Vector2 center, float disp )
{
var list = new List<CpMarker>();
if ( _maxExtent < 0.001f )
return list;
var line = RacingPath.Current?.Optimal;
var half = PanelSize * 0.5f;
var pxPerWorld = disp / _maxExtent;
var roadWidthWorld = RoadHalfWidth * 2f;
var lengthPx = MathF.Max( roadWidthWorld * pxPerWorld * 1.4f, 14f );
foreach ( var cp in Scene.GetAll<Checkpoint>() )
{
if ( !cp.IsValid() )
continue;
var n = WorldToNorm( cp.WorldPosition );
var sx = half + (n.x - center.x) * disp;
var sy = half + (n.y - center.y) * disp;
// Bar is perpendicular to the line tangent.
float angle = 0f;
if ( line is not null && line.IsValid )
{
var d = line.GetDistanceAtPosition( cp.WorldPosition );
var tan = line.GetTangentAtDistance( d );
// North-up map: screenX ~ -worldY, screenY ~ -worldX.
var tsx = -tan.y;
var tsy = -tan.x;
// Perpendicular = bar's long axis.
angle = MathF.Atan2( tsx, -tsy ) * (180f / MathF.PI);
}
list.Add( new CpMarker
{
X = sx,
Y = sy,
AngleDeg = angle,
LengthPx = lengthPx,
IsFinish = cp.IsFinishLine
} );
}
return list;
}
private struct BlipDot
{
public float X;
public float Y;
public Color Color;
public string Class;
public int Priority;
}
private List<BlipDot> GetBlipDots( Vector2 center, float disp )
{
var dots = new List<BlipDot>();
var half = PanelSize * 0.5f;
var maxR = half - 8f; // keep dots inside the round clip
// Off-screen blips clamp to the rim.
Vector2 Project( Vector3 world )
{
var n = WorldToNorm( world );
var off = new Vector2( (n.x - center.x) * disp, (n.y - center.y) * disp );
if ( off.Length > maxR )
off = off.Normal * maxR;
return new Vector2( half + off.x, half + off.y );
}
// GetAllComponents returns only enabled components.
foreach ( var blip in Scene.GetAllComponents<IMinimapBlip>() )
{
if ( blip is not Component c || !c.IsValid() || !blip.ShowOnMinimap )
continue;
var p = Project( c.WorldPosition );
dots.Add( new BlipDot
{
X = p.x,
Y = p.y,
Color = blip.BlipColor,
Class = blip.BlipClass,
Priority = blip.BlipPriority
} );
}
// Draw order: ghost < items < racers < local player.
dots.Sort( ( x, y ) => x.Priority.CompareTo( y.Priority ) );
return dots;
}
/// <summary>
/// Normalized [0,1] texel coords for a world position (north-up).
/// </summary>
private Vector2 WorldToNorm( Vector3 w )
{
// North-up: world +X = screen up, world +Y = screen left.
var nu = 0.5f - (w.y - _center.y) / _maxExtent;
var nv = 0.5f - (w.x - _center.x) / _maxExtent;
return new Vector2( nu, nv );
}
protected override int BuildHash()
{
return HashCode.Combine( _texture != null, Time.Now );
}
}
@using Sandbox;
@using Sandbox.UI;
@using Machines.Player;
@using Machines.Components;
@using Machines.GameModes;
@namespace Machines.UI
@inherits Panel
<root class="score-hud">
@{
var car = VisibleCar();
if ( car is not null )
{
var score = car.Score;
<div class="score-stack" style="opacity: @ComboAlpha( score ).ToString( "0.###", System.Globalization.CultureInfo.InvariantCulture );">
<div class="combo-value">@score.ComboScore</div>
@foreach ( var entry in score.Entries.OrderByDescending( e => e.LastTime ) )
{
<div class="combo-entry">
<span class="entry-source">@entry.Source</span>
<span class="entry-amount">+@entry.Amount</span>
</div>
}
</div>
}
}
</root>
@code
{
// Tail of the combo timeout over which the whole stack fades out (seconds).
private const float FadeTail = 0.4f;
/// <summary>
/// Local car to show, only while a combo is running; null hides the overlay (so it never
/// shows on start, and disappears once the chain lapses). Mirrors CarBoostHud's gating.
/// </summary>
private Car VisibleCar()
{
if ( !BaseGameMode.Current.IsValid() || BaseGameMode.Current.State != GameModeState.Playing )
return null;
if ( Game.ActiveScene?.Get<SpectatorCamera>()?.IsActive ?? false )
return null;
var car = Car.Local;
if ( !car.IsValid() || car.Autopilot )
return null;
if ( !car.Score.IsValid() || !car.Score.IsComboActive )
return null;
return car;
}
// Full opacity until the last FadeTail seconds of the combo, then ramp to zero.
private static float ComboAlpha( CarScore score )
{
var remaining = CarScore.ComboTimeout - score.SinceLastGain;
return MathX.Clamp( remaining / FadeTail, 0f, 1f );
}
protected override int BuildHash() => HashCode.Combine( Time.Now );
}
@using System;
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox
<root>
<div class="timer">@Timer.ToString()</div>
<div class="title" style="color: @(IsFailed ? FaildColor.Hex : NormalColor.Hex);">@TextOnScreen</div>
</root>
@code
{
[Property]
public Color NormalColor;
[Property]
public Color FaildColor;
bool IsFailed = false;
bool IsStarted = false;
string TextOnScreen = "Click before start!";
float Timer = 0;
protected override void OnUpdate()
{
if ( !IsStarted)
{
if ( Input.Pressed( "attack1" ))
{
IsStarted = true;
TextOnScreen = "Just chill and don't move your mouse...";
}
return;
}
if ( Mouse.Delta.x != 0 && Mouse.Delta.y != 0 || Input.Pressed("attack1") || Input.Pressed("attack2") )
{
IsFailed = true;
TextOnScreen = "YOUR FUCKED, IDIOT!!!";
}
if( !IsFailed )
Timer += Time.Delta;
base.OnUpdate();
}
protected override int BuildHash() => System.HashCode.Combine( TextOnScreen, NormalColor, FaildColor, Timer );
}
@using System;
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox
<root>
@if(_active)
{
<div class="survival-tut-bg"></div>
<div class="survival-tut-card">
<div class="tut-step-count">@(_step + 1) / @_steps.Length</div>
<div class="survival-tut-title">@_steps[_step].Title</div>
<div class="survival-tut-text">@_steps[_step].Text</div>
@if(_steps[_step].ShowGridDemo)
{
<div class="survival-demo-grid">
...
</div>
<div class="survival-demo-caption">@_demoCaption</div>
}
<button class="survival-tut-close" onclick=@NextStep>
@(_step == _steps.Length - 1 ? "▶ Let's Survive!" : "Got it →")
</button>
</div>
}
</root>
@code {
public static SurvivalTutorial Instance { get; private set; }
public bool IsActive => _active;
bool _active = false;
int _step = 0;
struct SurvivalTutStep
{
public string Title;
public string Text;
public bool ShowGridDemo;
}
static readonly SurvivalTutStep[] _steps = new SurvivalTutStep[]
{
new SurvivalTutStep {
Title = "Welcome to Survival! ⚡",
Text = "No waves here — chests spawn continuously and slide downward. Your goal: last as long as possible.",
ShowGridDemo = false
},
new SurvivalTutStep {
Title = "It Gets Faster... 🔥",
Text = "Chests spawn every couple seconds at first. After about a minute, spawn rate speeds up and more chests appear at once. Stay sharp — it never really stops.",
ShowGridDemo = false
},
new SurvivalTutStep {
Title = "Block the Slide! 🛡️",
Text = "Here's the key trick: a placed bomb blocks a chest from sliding into its column. Use this to buy time and set up bigger chain reactions before detonating.",
ShowGridDemo = true
},
new SurvivalTutStep {
Title = "Survive as Long as You Can 💀",
Text = "One chest reaching the bottom row ends your run. Place smart, chain big, and see how long you can hold on!",
ShowGridDemo = false
},
};
// ── Mini grid demo animation state ──
float _demoTimer = 0f;
int _demoPhase = 0; // 0=chest falling, 1=bomb placed blocks it, 2=hold
string _demoCaption = "A chest is about to slide down...";
protected override void OnStart()
{
Instance = this;
_active = false;
}
public void Activate()
{
_step = 0;
_active = true;
_demoPhase = 0;
_demoTimer = 0f;
StateHasChanged();
}
protected override void OnUpdate()
{
if (!_active || !_steps[_step].ShowGridDemo) return;
_demoTimer += Time.Delta;
// Simple 3-phase loop: chest falls (0-1.2s) -> bomb blocks it (1.2-2.4s) -> hold (2.4-3.6s) -> reset
if (_demoTimer < 1.2f)
{
_demoPhase = 0;
_demoCaption = "A chest slides down each cycle...";
}
else if (_demoTimer < 2.4f)
{
_demoPhase = 1;
_demoCaption = "...but a placed bomb blocks it from moving further!";
}
else if (_demoTimer < 3.6f)
{
_demoPhase = 2;
_demoCaption = "Now you have time to plan your chain reaction.";
}
else
{
_demoTimer = 0f;
}
StateHasChanged();
}
// Simple fixed layout: chest at (0,1) falling toward bomb at (2,1)
string GetDemoCellClass(int r, int c)
{
if (c != 1) return "empty";
if (_demoPhase == 0)
{
// chest animates from row 0 toward row 1
if (r == 0) return "chest";
if (r == 2) return "bomb";
return "empty";
}
else
{
// blocked — chest sits right above the bomb, doesn't pass
if (r == 1) return "chest";
if (r == 2) return "bomb";
return "empty";
}
}
string GetDemoCellIcon(int r, int c)
{
var cls = GetDemoCellClass(r, c);
return cls switch {
"chest" => "🎁",
"bomb" => "💣",
_ => ""
};
}
void NextStep()
{
_step++;
Sound.Play("UI_Button");
if (_step >= _steps.Length)
{
_active = false;
ChainReactionGame.Instance?.UnfreezeSurvivalSpawn(); // new public method
StateHasChanged();
return;
}
_demoTimer = 0f;
_demoPhase = 0;
StateHasChanged();
}
protected override int BuildHash() =>
System.HashCode.Combine(_active, _step, _demoPhase);
}@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
<root class="tt">
<div class="icon">@Icon</div>
<div class="title">@Title</div>
<div class="description">@Description</div>
</root>
@code
{
[Parameter] public string Title { get; set; }
[Parameter] public string Icon { get; set; }
[Parameter] public string Description { get; set; }
}
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Mounting;
@inherits Panel
@namespace Sandbox
<root>
<Button class="menu-action primary wide" Icon="💾" Text="#spawnmenu.dupes.save_button" Disabled=@( !CanSaveDupe() ) @onclick=@MakeSave></Button>
</root>
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Mounting;
@inherits Panel
@namespace Sandbox
<root>
<div class="title">@Item.Title</div>
<div class="author">
<div class="avatar" style="background-image: url('@Item.Owner.Avatar');"></div>
<div class="name">@Item.Owner.Name</div>
</div>
</root>
@code
{
public Storage.QueryItem Item { get; set; }
public override bool WantsDrag => true;
protected override void OnParametersSet()
{
Style.SetBackgroundImage( Item.Preview );
}
protected override void OnMouseDown( MousePanelEvent e )
{
if ( e.MouseButton == MouseButtons.Right )
{
var menu = MenuPanel.Open( this );
SpawnlistData.PopulateContextMenu( menu, new SpawnlistItem
{
Ident = SpawnlistItem.MakeIdent( "dupe", Item.Id.ToString(), "workshop" ),
Title = Item.Title,
Icon = Item.Preview,
} );
return;
}
base.OnMouseDown( e );
}
protected override async void OnDragStart( DragEvent e )
{
var data = new DragData
{
Type = "dupe",
Icon = Item.Preview,
Title = Item.Title,
Source = this
};
DragHandler.StartDragging( data );
var installed = await Item.Install();
if ( installed is null ) return;
data.Data = installed.Files.ReadAllText( "/dupe.json" );
}
protected override void OnDragEnd(DragEvent e)
{
if ( DragHandler.IsDragging )
{
DragHandler.StopDragging();
}
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
@if ( LastResult is null )
{
// loading
return;
}
<SpawnMenuContent>
<Header>
<SpawnMenuToolbar>
<Left>
<TextEntry Placeholder="#spawnmenu.common.search" class="filter menu-input" Value:bind=@Filter />
</Left>
<Right>
<DropDown Value:bind=@SortOrder />
</Right>
</SpawnMenuToolbar>
</Header>
<Body>
<VirtualGrid Items=@( LastResult.Packages ) ItemSize=@(120)>
<Item Context="item">
@if (item is Package entry)
{
<SpawnMenuIcon Ident=@($"prop:{entry.FullIdent}") Title="@entry.Title"></SpawnMenuIcon>
}
</Item>
</VirtualGrid>
</Body>
</SpawnMenuContent>
@code
{
public string Category { get; set; } = "";
private string Filter
{
get;
set { field = value; Rebuild(); }
}
public PackageSortMode SortOrder
{
get;
set { field = value; Rebuild(); }
}
protected override void OnParametersSet()
{
SortOrder = PackageSortMode.Popular;
Rebuild();
}
Package.FindResult LastResult;
async void Rebuild()
{
var query = $"sort:{SortOrder.ToIdentifier()} type:model";
if ( !string.IsNullOrEmpty( Filter ) ) query += $" {Filter} ";
if ( !string.IsNullOrEmpty( Category ) ) query += $" category:{Category}";
LastResult = await Package.FindAsync( query );
StateHasChanged();
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
@{
var data = GetData();
}
<SpawnMenuContent>
<Header>
<SpawnMenuToolbar>
<Left>
<h2>@data.Name</h2>
</Left>
<Right>
@{
var workshopId = Entry.GetMeta( "_workshopId", 0u );
var isPublished = workshopId > 0;
}
<div class="menu-icon-toggle-group">
@if ( isPublished )
{
<IconPanel Tooltip="#spawnmenu.spawnlist.copy_url" Text="link" @onclick=@( () => Clipboard.SetText( $"https://steamcommunity.com/sharedfiles/filedetails/?id={workshopId}" ) ) />
<IconPanel Tooltip="#spawnmenu.spawnlist.sync" Text="sync" @onclick=@( () => OnPublish() ) />
}
else
{
<IconPanel Tooltip="#spawnmenu.spawnlist.publish" Text="cloud_upload" @onclick=@( () => OnPublish() ) />
}
@if ( !Entry.Files.IsReadOnly )
{
<IconPanel Tooltip="#spawnmenu.spawnlist.delete" Text="delete" @onclick=@( () => OnDelete() ) />
}
else
{
<IconPanel Tooltip="#spawnmenu.spawnlist.remove" Text="delete" @onclick=@( () => OnUninstall() ) />
}
</div>
</Right>
</SpawnMenuToolbar>
</Header>
<Body>
@if ( data.Items.Count == 0 )
{
<div class="empty-state">
<p>#spawnmenu.spawnlist.empty_title</p>
<p>#spawnmenu.spawnlist.empty_instructions</p>
</div>
}
else
{
<VirtualGrid [email protected] ItemSize=@(120)>
<Item Context="item">
@if ( item is SpawnlistItem spawnItem )
{
<SpawnMenuIcon Ident="@spawnItem.Ident" Title="@spawnItem.Title" Icon="@spawnItem.Icon" />
}
</Item>
</VirtualGrid>
}
</Body>
</SpawnMenuContent>
@code
{
public Storage.Entry Entry { get; set; }
SpawnlistData _cachedData;
RealTimeSince _lastRefresh;
SpawnlistData GetData()
{
if ( _cachedData == null )
RefreshCache();
return _cachedData;
}
public void RefreshCache()
{
_cachedData = SpawnlistData.Load( Entry );
_lastRefresh = 0;
}
public override void Tick()
{
base.Tick();
// Periodically check for changes from other tabs
if ( _lastRefresh > 1f )
{
var fresh = SpawnlistData.Load( Entry );
if ( fresh.Items.Count != _cachedData?.Items?.Count )
{
_cachedData = fresh;
StateHasChanged();
}
_lastRefresh = 0;
}
}
protected override int BuildHash() => HashCode.Combine( _cachedData?.Items?.Count );
void OnPublish()
{
if ( Entry is null ) return;
SpawnlistData.Publish( Entry );
}
void OnDelete()
{
if ( Entry is null ) return;
var page = Ancestors.OfType<SpawnlistsPage>().FirstOrDefault();
page?.Collection.Delete( Entry );
}
void OnUninstall()
{
if ( Entry is null ) return;
var workshopId = Entry.GetMeta( "_workshopId", 0ul );
var page = Ancestors.OfType<SpawnlistsPage>().FirstOrDefault();
page?.Collection.Uninstall( workshopId );
}
}
@using Sandbox;
@using Sandbox.UI;
@namespace Sandbox
@inherits Panel
<root>
<div class="menu-segmented-group">
@{
var activeMode = SpawnMenuHost.GetActiveMode();
foreach ( var mode in Game.TypeLibrary.GetTypesWithAttribute<SpawnMenuHost.SpawnMenuMode>().OrderBy( x => x.Type.Order ) )
{
if ( !mode.Attribute.CheckCondition() ) continue;
var activeClass = mode.Type.TargetType == activeMode?.GetType() ? "active" : "";
<div class="menu-mode-button @activeClass" @onclick="@( () => SpawnMenuHost.SwitchMode( mode.Type.Name ) )">
<div class="icon">@mode.Type.Icon</div>
<div class="title">@mode.Type.Title</div>
</div>
}
}
</div>
</root>
@code
{
protected override int BuildHash() => HashCode.Combine( SpawnMenuHost.GetActiveMode(), Game.TypeLibrary.GetTypesWithAttribute<SpawnMenuHost.SpawnMenuMode>() );
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace VehicleProto
@* Lab / tuning (design board 1c) — toggled by T ("Debug"). Writes live onto the running car.
Left: grouped dial panel. Right: live suspension rig (bound to VehicleWheel). *@
<root>
@if ( UiState.TuningOpen && Car.IsValid() )
{
<div class="panel">
<div class="phead">
<div class="ptitle">Tuning — @CarName</div>
<div class="pactions">
<div class="qbtn" onclick=@ToggleTuneHelp>?</div>
<div class="mono kbd">T</div>
</div>
</div>
<div class="tabs">
@foreach ( var g in Groups )
{
var gg = g;
<div class="mono tab @(_tab == gg ? "on" : "")" onclick=@( () => SetTab( gg ) )>@gg</div>
}
</div>
<div class="dials">
@foreach ( var p in ActiveParams )
{
var pp = p;
<div class="dial">
<div class="drow">
<div class="dname">@p.Name @if ( IsDirty( p ) ) { <span class="ddot"></span> }</div>
<div class="mono dval">@p.Get().ToString( p.Fmt )@p.Suffix</div>
</div>
<div class="dctl">
<div class="mono step" onclick=@( () => Adjust( pp, -1 ) )>−</div>
<div class="track"
onmousedown=@( e => TrackScrub( pp, e, true ) )
onmousemove=@( e => TrackScrub( pp, e, false ) )>
<div class="fill" style="width: @UiFmt.Pct( Frac( pp ) * 100f )%"></div>
</div>
<div class="mono step" onclick=@( () => Adjust( pp, +1 ) )>+</div>
</div>
</div>
}
</div>
<div class="dirty @(DirtyCount > 0 ? "" : "clean")">
<span class="ddot"></span>
<span class="dtext">@DirtyText</span>
</div>
<div class="presets">
<div class="prow phdr">
<div class="mono plabel">Saved tunes · @CarName</div>
<div class="btn primary psave" onclick=@SavePreset>Save current</div>
</div>
@if ( CarPresets.Count == 0 )
{
<div class="mono pempty">No saved tunes for this car yet — tweak the dials, then Save current.</div>
}
else
{
@* Scrollable list container (Issue 3): header/Save stay fixed above; only the ROWS
scroll, capped at ~3 rows so a long list can't scrunch the panel into the HUD. *@
<div class="plist">
@foreach ( var preset in CarPresets )
{
var pr = preset;
<div class="prow">
<div class="pname" onclick=@( () => LoadPreset( pr ) )>@pr.Name</div>
<div class="mono pedit" onclick=@( () => BeginRename( pr ) )>Edit</div>
<div class="mono pload" onclick=@( () => LoadPreset( pr ) )>Load</div>
<div class="mono pdel" onclick=@( () => DeletePreset( pr ) )>Del</div>
</div>
}
</div>
}
</div>
<div class="foot">
<div class="frow">
<div class="btn ghost" onclick=@ResetAll>Reset all</div>
<div class="btn primary" onclick=@( () => Car.Respawn() )>Respawn car · R</div>
</div>
<div class="mono fhint">changes apply instantly to the running car</div>
</div>
</div>
<div class="suspension">
<div class="shead">
<div class="stitle">Suspension</div>
<div class="mono scap">travel · load</div>
</div>
<div class="rig">
@for ( int i = 0; i < Car.Wheels.Count && i < 4; i++ )
{
var w = Car.Wheels[i];
var bottomed = IsBottomed( w );
<div class="scol">
<div class="sbar @(bottomed ? "bottom" : "")">
<div class="sfill @(bottomed ? "bottom" : "")" style="height: @UiFmt.Pct( LoadFrac( w ) * 100f )%"></div>
</div>
<div class="mono slbl @(bottomed ? "bottom" : "")">@WheelLabel( i )@(bottomed ? " · BOTTOM" : "")</div>
<div class="mono skg">@LoadKg( w ) kg</div>
</div>
}
</div>
</div>
}
</root>
@code {
// Car is set once at Mount and re-pointed on every live car swap by UiRig.Retarget. The panel
// caches param closures over ONE definition instance (BuildParams captures `def`), so a swap must
// invalidate that cache — otherwise the panel keeps displaying and EDITING the orphaned old def
// while the player drives a car built from a fresh definition instance (tester bug: redline dial
// had no effect after a Tab/menu car switch). The setter drops the cached params + tuning state so
// the next access rebuilds against the new car's own (untuned) definition.
VehicleController _car;
public VehicleController Car
{
get => _car;
set
{
if ( ReferenceEquals( _car, value ) )
return;
_car = value;
OnCarChanged();
}
}
void OnCarChanged()
{
_params = null; // force a rebuild of the param closures against the new def
_carPresets = null; // presets are per-car — re-scope the list to the active car
_gripScale = 1f; // the new car is untuned — grip multiplier resets to 1×
var def = _car?.Definition;
if ( def is not null )
{
_baseLat = def.LateralCurve;
_baseLong = def.LongitudinalCurve;
}
StateHasChanged();
}
static readonly string[] Groups = { "engine", "tires", "susp", "assists" };
string _tab = "engine";
class Param
{
public string Name;
public string Group = "engine";
public string Suffix = "";
public string Fmt = "F0";
public float Step, Min, Max;
public float Baseline;
public Func<float> Get;
public Action<float> Set;
}
List<Param> _params;
List<Param> Params => _params ??= BuildParams();
IEnumerable<Param> ActiveParams => Params.Where( p => p.Group == _tab );
string CarName => Car?.Definition?.Name ?? "Car";
TireCurve _baseLat, _baseLong;
float _gripScale = 1f;
// Shift points are absolute per-car constants in the definition, but the Redline dial must carry
// them so lowering redline below ShiftUpRpm doesn't limiter-stick the car (never upshifts). Capture
// each shift point's fraction of redline when params are (re)built for the car, then scale on Set.
float _shiftUpFrac = 0.92f, _shiftDownFrac = 0.35f;
// ---- player tune presets (Save / Load / Delete over the current dial set) ----
// Presets are per-car and persisted to FileSystem.Data (TunePresetStore), so they survive restart.
// The list is cached and re-scoped to the active car in OnCarChanged; _presetsRev bumps the
// BuildHash on any add/delete so the list re-renders even when the car is sitting still (the panel
// only auto-rebuilds while suspension load changes). Auto-named ("<car> tune N"): a live text field
// is impractical here because this panel rebuilds every frame off live load and would drop focus.
IReadOnlyList<TunePreset> _carPresets;
IReadOnlyList<TunePreset> CarPresets => _carPresets ??= TunePresetStore.ForCar( CarSwitcher.CurrentId( Car ) );
int _presetsRev;
void RefreshPresets()
{
_carPresets = null;
_presetsRev++;
StateHasChanged();
}
void SavePreset()
{
if ( !Car.IsValid() )
return;
var def = Car.Definition;
string carId = CarSwitcher.CurrentId( Car );
var preset = new TunePreset
{
CarId = carId,
Name = TunePresetStore.NextName( carId ),
PeakTorque = def.PeakTorque,
FinalDrive = def.FinalDrive,
LaunchBoost = def.LaunchBoost,
EngineBrakeTorque = def.EngineBrakeTorque,
RedlineRpm = def.RedlineRpm,
ShiftUpRpm = def.ShiftUpRpm, // coupled to redline — stored verbatim
ShiftDownRpm = def.ShiftDownRpm, // coupled to redline — stored verbatim
GripScale = _gripScale, // the multiplier, not the raw curve points
HandbrakeGripScale = def.HandbrakeGripScale,
BrakeTorque = def.BrakeTorque,
BrakeAssist = def.BrakeAssist,
HandbrakeTorque = def.HandbrakeTorque,
SpringRate = def.SpringRate,
DamperRate = def.DamperRate,
Mass = def.Mass,
GravityScale = GravityScale(), // scene property, captured for parity
SteerRateScale = def.SteerRateScale,
MaxSteerAngle = def.MaxSteerAngle,
HighSpeedSteerAngle = def.HighSpeedSteerAngle,
ReverseSpeedCap = def.ReverseSpeedCap,
SpinRecoveryAssist = def.SpinRecoveryAssist,
};
TunePresetStore.Add( preset );
RefreshPresets();
}
void LoadPreset( TunePreset preset )
{
if ( !Car.IsValid() || preset is null )
return;
// per-car guard: never apply a preset saved for a different car (defensive — the UI only ever
// lists the active car's presets, but a stale click or future importable file could mismatch).
if ( !string.Equals( preset.CarId, CarSwitcher.CurrentId( Car ), StringComparison.OrdinalIgnoreCase ) )
{
Log.Warning( $"[vp] tune preset '{preset.Name}' is for '{preset.CarId}', active car is "
+ $"'{CarSwitcher.CurrentId( Car )}' — not applying" );
return;
}
var def = Car.Definition;
def.PeakTorque = preset.PeakTorque;
def.FinalDrive = preset.FinalDrive;
def.LaunchBoost = preset.LaunchBoost;
def.EngineBrakeTorque = preset.EngineBrakeTorque;
// Redline + its coupled shift points: restore all three verbatim (identical to save time), then
// re-derive the fractions so a later Redline dial edit scales from THIS loaded baseline.
def.RedlineRpm = preset.RedlineRpm;
def.ShiftUpRpm = preset.ShiftUpRpm;
def.ShiftDownRpm = preset.ShiftDownRpm;
if ( preset.RedlineRpm > 0f )
{
_shiftUpFrac = preset.ShiftUpRpm / preset.RedlineRpm;
_shiftDownFrac = preset.ShiftDownRpm / preset.RedlineRpm;
}
def.HandbrakeGripScale = preset.HandbrakeGripScale;
def.BrakeTorque = preset.BrakeTorque;
def.BrakeAssist = preset.BrakeAssist;
def.HandbrakeTorque = preset.HandbrakeTorque;
def.SpringRate = preset.SpringRate;
def.DamperRate = preset.DamperRate;
def.Mass = preset.Mass;
def.SteerRateScale = preset.SteerRateScale;
def.MaxSteerAngle = preset.MaxSteerAngle;
def.HighSpeedSteerAngle = preset.HighSpeedSteerAngle;
def.ReverseSpeedCap = preset.ReverseSpeedCap;
def.SpinRecoveryAssist = preset.SpinRecoveryAssist;
// Grip: re-apply the scale through SetGrip so the tire curves are re-derived from the PRISTINE
// baseline (_baseLat/_baseLong), never from an already-scaled state. SetGrip also runs ApplyWheels,
// which pushes the new SpringRate/DamperRate/curves onto the live wheels.
SetGrip( preset.GripScale );
// Mass onto the live rigidbody + per-wheel static loads.
ApplyChassis();
// Gravity is a scene property — re-apply for parity with the saved state.
SetGravity( preset.GravityScale );
// The loaded state is now the reference the dirty markers diff against.
if ( _params is not null )
foreach ( var p in _params )
p.Baseline = p.Get();
StateHasChanged();
}
void DeletePreset( TunePreset preset )
{
TunePresetStore.Delete( preset );
RefreshPresets();
}
// Open the rename modal for this preset. The text field lives in a SEPARATE panel
// (TuneRenameOverlay) precisely because THIS panel rebuilds every frame off live suspension load and
// would drop a focused TextEntry each frame — see TuneRenameState for the full rationale. The commit
// mutates the preset in place; our list re-renders via _presetsRev-style BuildHash folding of
// TuneRenameState.Revision (below) so the new name shows even with the car standing still.
void BeginRename( TunePreset preset ) => TuneRenameState.Begin( preset );
void SetTab( string t ) { _tab = t; StateHasChanged(); }
// "?" header button: open/close the dial-explainer overlay. The overlay is a SEPARATE PanelComponent
// (TuneHelpOverlay) reading UiState.TuneHelpOpen, so this flag flip re-renders THAT panel on its own
// constant BuildHash — nothing here rebuilds mid-view. Clicking "?" again toggles it closed.
void ToggleTuneHelp() => UiState.TuneHelpOpen = !UiState.TuneHelpOpen;
float Frac( Param p )
{
if ( p.Max <= p.Min ) return 0f;
return Math.Clamp( (p.Get() - p.Min) / (p.Max - p.Min), 0f, 1f );
}
bool IsDirty( Param p ) => MathF.Abs( p.Get() - p.Baseline ) > p.Step * 0.25f;
int DirtyCount => Params.Count( IsDirty );
string DirtyText => DirtyCount == 0
? "matches saved definition"
: DirtyCount == 1 ? "1 dial differs from saved definition"
: $"{DirtyCount} dials differ from saved definition";
// ---- suspension rig bindings (live) ----
static readonly string[] WheelLabels = { "FL", "FR", "RL", "RR" };
string WheelLabel( int i ) => i < WheelLabels.Length ? WheelLabels[i] : $"W{i}";
float LoadFrac( VehicleWheel w ) => w.StaticLoad > 0f ? Math.Clamp( w.Load / (w.StaticLoad * 2f), 0f, 1f ) : 0f;
string LoadKg( VehicleWheel w ) => (w.Load / 9.81f).ToString( "F0" );
bool IsBottomed( VehicleWheel w ) => w.IsGrounded && w.SuspensionLength <= w.SuspensionTravel * 0.03f;
protected override void OnUpdate()
{
if ( Input.Pressed( "Debug" ) )
{
UiState.TuningOpen = !UiState.TuningOpen;
Mouse.Visibility = UiState.TuningOpen ? MouseVisibility.Visible : MouseVisibility.Hidden;
// The dial-help overlay only makes sense over an open panel — closing Tuning closes it too,
// so the explainer can't linger on screen with no dials behind it.
if ( !UiState.TuningOpen )
UiState.TuneHelpOpen = false;
}
// something else (or the engine) may reset cursor state every frame
if ( UiState.TuningOpen )
Mouse.Visibility = MouseVisibility.Visible;
}
void Adjust( Param p, int clicks )
{
float v = Math.Clamp( p.Get() + clicks * p.Step, p.Min, p.Max );
p.Set( v );
StateHasChanged();
}
// Click-to-jump + drag-to-scrub on the dial track. Mirrors the engine's own SliderControl mechanic
// (no native drag helper for a hand-rolled RenderFragment). `jump` is true on onmousedown (a bare
// click lands as jump-to-position) and false on onmousemove (scrub only while the track is actively
// pressed). Value = mouse LOCAL x over the track width, snapped to the param's Step, pushed through
// the SAME p.Set path as Adjust so Redline's ScaleShiftPoints, Grip's SetGrip etc. still apply.
// (KB gotcha g-ui-draggable-slider-tracks-click-jump-drag; verified drag pattern.)
void TrackScrub( Param p, Sandbox.UI.PanelEvent ev, bool jump )
{
if ( ev is not Sandbox.UI.MousePanelEvent e ) return;
var track = e.This;
if ( track is null ) return;
// onmousemove fires whether or not the button is held — only scrub while the track owns the press.
if ( !jump && !track.PseudoClass.HasFlag( Sandbox.UI.PseudoClass.Active ) ) return;
float w = track.Box.Rect.Width;
if ( w <= 0f ) return;
float frac = Math.Clamp( e.LocalPosition.x / w, 0f, 1f );
float v = p.Min + frac * (p.Max - p.Min);
if ( p.Step > 0f ) v = MathF.Round( v / p.Step ) * p.Step; // snap to the param's step (values stay clean)
v = Math.Clamp( v, p.Min, p.Max );
p.Set( v );
StateHasChanged();
}
void ResetAll()
{
// Reset to the CURRENT car's pristine preset, not always the hatch — otherwise resetting while
// driving a coupe/kart/pickup wrote hatch physics onto it. Presets are `=> new()` so each access
// is a fresh untouched instance (StationCarRegistry.ResolveCar via the canonical id table).
var fresh = VehiclePilot.ResolveCar( CarSwitcher.CurrentId( Car ) );
var def = Car.Definition;
def.PeakTorque = fresh.PeakTorque;
def.LaunchBoost = fresh.LaunchBoost;
def.FinalDrive = fresh.FinalDrive;
def.EngineBrakeTorque = fresh.EngineBrakeTorque;
def.RedlineRpm = fresh.RedlineRpm;
// Redline dial now carries the shift points (ScaleShiftPoints); restore them too, and re-capture
// the pristine fractions so a subsequent Redline edit scales from the reset baseline.
def.ShiftUpRpm = fresh.ShiftUpRpm;
def.ShiftDownRpm = fresh.ShiftDownRpm;
if ( fresh.RedlineRpm > 0f )
{
_shiftUpFrac = fresh.ShiftUpRpm / fresh.RedlineRpm;
_shiftDownFrac = fresh.ShiftDownRpm / fresh.RedlineRpm;
}
def.SteerRateScale = fresh.SteerRateScale;
def.MaxSteerAngle = fresh.MaxSteerAngle;
def.HighSpeedSteerAngle = fresh.HighSpeedSteerAngle;
def.ReverseSpeedCap = fresh.ReverseSpeedCap;
def.SpinRecoveryAssist = fresh.SpinRecoveryAssist;
def.BrakeTorque = fresh.BrakeTorque;
def.BrakeAssist = fresh.BrakeAssist;
def.HandbrakeGripScale = fresh.HandbrakeGripScale;
def.HandbrakeTorque = fresh.HandbrakeTorque;
def.SpringRate = fresh.SpringRate;
def.DamperRate = fresh.DamperRate;
def.Mass = fresh.Mass;
def.LateralCurve = fresh.LateralCurve;
def.LongitudinalCurve = fresh.LongitudinalCurve;
_gripScale = 1f;
_baseLat = fresh.LateralCurve;
_baseLong = fresh.LongitudinalCurve;
ApplyChassis();
ApplyWheels();
SetGravity( 1.1f );
if ( _params is not null )
foreach ( var p in _params )
p.Baseline = p.Get();
StateHasChanged();
}
// push def values onto the live rigidbody + wheels (factory copies them at spawn)
void ApplyChassis()
{
var def = Car.Definition;
var body = Car.Components.Get<Rigidbody>();
if ( body.IsValid() )
body.MassOverride = def.Mass;
foreach ( var w in Car.Wheels )
w.StaticLoad = def.Mass * 9.81f / 4f;
}
void ApplyWheels()
{
var def = Car.Definition;
foreach ( var w in Car.Wheels )
{
w.SpringRate = def.SpringRate;
w.DamperRate = def.DamperRate;
w.LateralCurve = def.LateralCurve;
w.LongitudinalCurve = def.LongitudinalCurve;
}
}
static TireCurve Scaled( TireCurve c, float k ) =>
new( c.PeakSlip, c.PeakGrip * k, c.TailSlip, c.TailGrip * k );
void SetGrip( float k )
{
_gripScale = k;
var def = Car.Definition;
def.LateralCurve = Scaled( _baseLat, k );
def.LongitudinalCurve = Scaled( _baseLong, k );
ApplyWheels();
}
float GravityScale() => Scene.PhysicsWorld.Gravity.Length / (9.81f * Units.MetersToUnits);
void SetGravity( float k ) => Scene.PhysicsWorld.Gravity = Vector3.Down * 9.81f * k * Units.MetersToUnits;
// Preserve each shift point's original fraction of redline, and never let ShiftUpRpm reach the
// limiter (clamp to 95% of redline) or the car can't upshift out of the rev ceiling.
void ScaleShiftPoints( CarDefinition def, float redline )
{
def.ShiftUpRpm = MathF.Min( redline * _shiftUpFrac, redline * 0.95f );
def.ShiftDownRpm = redline * _shiftDownFrac;
}
List<Param> BuildParams()
{
var def = Car.Definition;
_baseLat = def.LateralCurve;
_baseLong = def.LongitudinalCurve;
// capture the pristine shift-point fractions for THIS car (def is untouched at build time)
if ( def.RedlineRpm > 0f )
{
_shiftUpFrac = def.ShiftUpRpm / def.RedlineRpm;
_shiftDownFrac = def.ShiftDownRpm / def.RedlineRpm;
}
var list = new List<Param>
{
// --- engine ---
new() { Group = "engine", Name = "Engine torque", Suffix = " Nm", Step = 20f, Min = 100f, Max = 900f,
Get = () => def.PeakTorque, Set = v => def.PeakTorque = v },
new() { Group = "engine", Name = "Final drive", Fmt = "F1", Step = 0.1f, Min = 2.5f, Max = 6.5f,
Get = () => def.FinalDrive, Set = v => def.FinalDrive = v },
new() { Group = "engine", Name = "Launch boost", Fmt = "F1", Suffix = "×", Step = 0.1f, Min = 1f, Max = 4f,
Get = () => def.LaunchBoost, Set = v => def.LaunchBoost = v },
new() { Group = "engine", Name = "Engine brake", Suffix = " Nm", Step = 5f, Min = 0f, Max = 60f,
Get = () => def.EngineBrakeTorque, Set = v => def.EngineBrakeTorque = v },
new() { Group = "engine", Name = "Redline", Suffix = " rpm", Step = 100f, Min = 5000f, Max = 9500f,
Get = () => def.RedlineRpm, Set = v => { def.RedlineRpm = v; ScaleShiftPoints( def, v ); } },
// --- tires / brakes ---
new() { Group = "tires", Name = "Grip", Fmt = "F2", Suffix = "×", Step = 0.05f, Min = 0.6f, Max = 2.2f,
Get = () => _gripScale, Set = SetGrip },
new() { Group = "tires", Name = "Drift grip (handbrake)", Fmt = "F2", Suffix = "×", Step = 0.05f, Min = 0.15f, Max = 1f,
Get = () => def.HandbrakeGripScale, Set = v => def.HandbrakeGripScale = v },
new() { Group = "tires", Name = "Brake torque", Suffix = " Nm", Step = 200f, Min = 1500f, Max = 8000f,
Get = () => def.BrakeTorque, Set = v => def.BrakeTorque = v },
new() { Group = "tires", Name = "Brake assist", Fmt = "F1", Suffix = " m/s²", Step = 0.5f, Min = 0f, Max = 12f,
Get = () => def.BrakeAssist, Set = v => def.BrakeAssist = v },
new() { Group = "tires", Name = "Handbrake torque", Suffix = " Nm", Step = 250f, Min = 1000f, Max = 9000f,
Get = () => def.HandbrakeTorque, Set = v => def.HandbrakeTorque = v },
// --- suspension ---
new() { Group = "susp", Name = "Spring rate", Suffix = " N/m", Step = 2000f, Min = 15000f, Max = 60000f,
Get = () => def.SpringRate, Set = v => { def.SpringRate = v; ApplyWheels(); } },
new() { Group = "susp", Name = "Damper rate", Step = 200f, Min = 800f, Max = 6000f,
Get = () => def.DamperRate, Set = v => { def.DamperRate = v; ApplyWheels(); } },
new() { Group = "susp", Name = "Mass", Suffix = " kg", Step = 50f, Min = 400f, Max = 2500f,
Get = () => def.Mass, Set = v => { def.Mass = v; ApplyChassis(); } },
new() { Group = "susp", Name = "Gravity", Fmt = "F2", Suffix = " g", Step = 0.05f, Min = 0.6f, Max = 2.2f,
Get = GravityScale, Set = SetGravity },
// --- steering / assists ---
new() { Group = "assists", Name = "Steer speed", Fmt = "F1", Suffix = "×", Step = 0.1f, Min = 0.5f, Max = 3f,
Get = () => def.SteerRateScale, Set = v => def.SteerRateScale = v },
new() { Group = "assists", Name = "Steer lock (low speed)", Suffix = "°", Step = 1f, Min = 20f, Max = 45f,
Get = () => def.MaxSteerAngle, Set = v => def.MaxSteerAngle = v },
new() { Group = "assists", Name = "Steer lock (high speed)", Suffix = "°", Step = 1f, Min = 6f, Max = 20f,
Get = () => def.HighSpeedSteerAngle, Set = v => def.HighSpeedSteerAngle = v },
new() { Group = "assists", Name = "Reverse speed cap", Fmt = "F1", Suffix = " m/s", Step = 0.5f, Min = 3f, Max = 15f,
Get = () => def.ReverseSpeedCap, Set = v => def.ReverseSpeedCap = v },
new() { Group = "assists", Name = "Spin recovery", Fmt = "F1", Suffix = " m/s²", Step = 0.5f, Min = 0f, Max = 12f,
Get = () => def.SpinRecoveryAssist, Set = v => def.SpinRecoveryAssist = v },
};
foreach ( var p in list )
p.Baseline = p.Get();
return list;
}
protected override int BuildHash()
{
if ( !UiState.TuningOpen )
return 0;
var h = new HashCode();
h.Add( _tab );
h.Add( DirtyCount );
h.Add( _presetsRev ); // re-render the presets list on save/delete even when the car is still
h.Add( TuneRenameState.Revision ); // ...and on a committed rename (the preset's Name changed in place)
if ( _params is not null )
foreach ( var p in _params )
h.Add( (int)(p.Get() * 100f) );
if ( Car.IsValid() )
foreach ( var w in Car.Wheels )
{
h.Add( (int)(w.Load * 0.1f) );
h.Add( (int)(w.SuspensionLength * 1000f) );
}
return h.ToHashCode();
}
}
@using System;
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
<root>
<div class="crosshair" style="
position: absolute;
left: @( IsPercentage ? $"{Position.x}%" : Position.x );
top: @( IsPercentage ? $"{Position.y}%" : Position.y );
transform: translate(-50%, -50%);
">
<div class="center-dot-border-wrapper" style="
display: @(CenterDot ? "flex" : "none");
border: @(Outline ? $"{OutlineThickness}px solid rgba(0, 0, 0, {OutlineOpacity})" : "none");
z-index: 100;
">
<div class="center-dot" style="
background-color: rgba(@(Color.r * 255), @(Color.g * 255), @(Color.b * 255), @(CenterDotOpacity));
padding: @(CenterDotThickness)px;
"></div>
</div>
<div class="inner-top-border-wrapper" style="
position: absolute;
top: -@(InnerLinesOffset)px;
left: 50%;
transform: translateX(-50%);
display:@(ShowInnerLines ? "flex" : "none");
border: @(Outline ? $"{OutlineThickness}px solid rgba(0, 0, 0, {OutlineOpacity})" : "none");
">
<div class="inner-line top" style="
background-color: rgba(@(Color.r * 255), @(Color.g * 255), @(Color.b * 255), @(InnerLineOpacity));
padding-left: @(InnerLineThickness)px;
padding-top: @(InnerLineLenght)px;
"></div>
</div>
<div class="inner-bottom-border-wrapper" style="
position: absolute;
bottom: -@(InnerLinesOffset)px;
left: 50%;
transform: translateX(-50%);
display:@(ShowInnerLines ? "flex" : "none");
border: @(Outline ? $"{OutlineThickness}px solid rgba(0, 0, 0, {OutlineOpacity})" : "none");
">
<div class="inner-line bottom" style="
background-color: rgba(@(Color.r * 255), @(Color.g * 255), @(Color.b * 255), @(InnerLineOpacity));
padding-left: @(InnerLineThickness)px;
padding-bottom: @(InnerLineLenght)px;
"></div>
</div>
<div class="inner-left-border-wrapper" style="
position: absolute;
left: -@(InnerLinesOffset)px;
top: 50%;
transform: translateY(-50%);
display:@(ShowInnerLines ? "flex" : "none");
border: @(Outline ? $"{OutlineThickness}px solid rgba(0, 0, 0, {OutlineOpacity})" : "none");
">
<div class="inner-line left" style="
background-color: rgba(@(Color.r * 255), @(Color.g * 255), @(Color.b * 255), @(InnerLineOpacity));
padding-top: @(InnerLineThickness)px;
padding-left: @(InnerLineLenght)px;
"></div>
</div>
<div class="inner-right-border-wrapper" style="
position: absolute;
right: -@(InnerLinesOffset)px;
top: 50%;
transform: translateY(-50%);
display: @(ShowInnerLines ? "flex" : "none");
border: @(Outline ? $"{OutlineThickness}px solid rgba(0, 0, 0, {OutlineOpacity})" : "none");
">
<div class="inner-line right" style="
background-color: rgba(@(Color.r * 255), @(Color.g * 255), @(Color.b * 255), @(InnerLineOpacity));
padding-top: @(InnerLineThickness)px;
padding-right: @(InnerLineLenght)px;
"></div>
</div>
<div class="outer-top-border-wrapper" style="
position: absolute;
top: -@(OuterLineOffset)px;
left: 50%;
transform: translateX(-50%);
display: @(ShowOuterLines ? "flex" : "none");
border: @(Outline ? $"{OutlineThickness}px solid rgba(0, 0, 0, {OutlineOpacity})" : "none");
">
<div class="outer-line top" style="
background-color: rgba(@(Color.r * 255), @(Color.g * 255), @(Color.b * 255), @(OuterLineOpacity));
padding-left: @(OuterLineThickness)px;
padding-top: @(OuterLineLenght)px;
"></div>
</div>
<div class="outer-bottom-border-wrapper" style="
position: absolute;
bottom: -@(OuterLineOffset)px;
left: 50%;
transform: translateX(-50%);
display: @(ShowOuterLines ? "flex" : "none");
border: @(Outline ? $"{OutlineThickness}px solid rgba(0, 0, 0, {OutlineOpacity})" : "none");
">
<div class="outer-line bottom" style="
background-color: rgba(@(Color.r * 255), @(Color.g * 255), @(Color.b * 255), @(OuterLineOpacity));
padding-left: @(OuterLineThickness)px;
padding-bottom: @(OuterLineLenght)px;
"></div>
</div>
<div class="outer-left-border-wrapper" style="
position: absolute;
left: -@(OuterLineOffset)px;
top: 50%;
transform: translateY(-50%);
display: @(ShowOuterLines ? "flex" : "none");
border: @(Outline ? $"{OutlineThickness}px solid rgba(0, 0, 0, {OutlineOpacity})" : "none");
">
<div class="outer-line left" style="
background-color: rgba(@(Color.r * 255), @(Color.g * 255), @(Color.b * 255), @(OuterLineOpacity));
padding-top: @(OuterLineThickness)px;
padding-left: @(OuterLineLenght)px;
"></div>
</div>
<div class="outer-right-border-wrapper" style="
position: absolute;
right: -@(OuterLineOffset)px;
top: 50%;
transform: translateY(-50%);
display: @(ShowOuterLines ? "flex" : "none");
border: @(Outline ? $"{OutlineThickness}px solid rgba(0, 0, 0, {OutlineOpacity})" : "none");
">
<div class="outer-line right" style="
background-color: rgba(@(Color.r * 255), @(Color.g * 255), @(Color.b * 255), @(OuterLineOpacity));
padding-top: @(OuterLineThickness)px;
padding-right: @(OuterLineLenght)px;
"></div>
</div>
</div>
</root>
@code
{
// Position properties
[Property]
[Category("Position")] public bool IsPercentage { get; set; } = true;
[Property]
[Category("Position")] public Vector2 Position { get; set; } = new Vector2(50, 50);
// Crosshair properties
[Property]
[Category("Crosshair")] public Color Color { get; set; } = Color.FromRgb(0x2EFF00);
[Property]
[Category("Crosshair")] public bool Outline { get; set; } = true;
[Property]
[Range(0f, 1f, 0.001f)]
[Category("Crosshair")] public float OutlineOpacity { get; set; } = 1f;
[Property]
[Range(1, 10, 1)]
[Category("Crosshair")] public int OutlineThickness { get; set; } = 2;
[Property]
[Category("Crosshair")] public bool CenterDot { get; set; } = true;
[Property]
[Range(0f, 1f, 0.001f)]
[Category("Crosshair")] public float CenterDotOpacity { get; set; } = 1f;
[Property]
[Range(1, 10, 1)]
[Category("Crosshair")] public int CenterDotThickness { get; set; } = 1;
// Inner Lines properties
[Property]
[Category("Inner Lines")] public bool ShowInnerLines { get; set; } = true;
[Property]
[Range(0f, 1f, 0.001f)]
[Category("Inner Lines")] public float InnerLineOpacity { get; set; } = 1f;
[Property]
[Range(0, 20, 1)]
[Category("Inner Lines")] public int InnerLineLenght { get; set; } = 10;
[Property]
[Range(0, 10, 1)]
[Category("Inner Lines")] public int InnerLineThickness { get; set; } = 1;
[Property]
[Range(0, 20, 1)]
[Category("Inner Lines")] public int InnerLinesOffset { get; set; } = 2;
// Outer Lines properties
[Property]
[Category("Outer Lines")] public bool ShowOuterLines { get; set; } = false;
[Property]
[Range(0f, 1f, 0.001f)]
[Category("Outer Lines")] public float OuterLineOpacity { get; set; } = 0.5f;
[Property]
[Range(0, 20, 1)]
[Category("Outer Lines")] public int OuterLineLenght { get; set; }
[Property]
[Range(0, 10, 1)]
[Category("Outer Lines")] public int OuterLineThickness { get; set; }
[Property]
[Range(0, 20, 1)]
[Category("Outer Lines")] public int OuterLineOffset { get; set; }
// Code properties
[Property]
[Category("Code")] public string Code { get; set; }
// Member variables
private string _cachedCode = null;
public string EncodeCrosshairParameters()
{
// Convert boolean values to 1 or 0
string EncodeBool(bool value) => value ? "1" : "0";
// Convert float values to a shortened string representation (up to 2 decimal places)
string EncodeFloat(float value) => Math.Round(value, 2).ToString("0.##");
// Convert integers directly to string
string EncodeInt(int value) => value.ToString();
// Concatenate all parameters into a shortened format
var parameters = $"{EncodeBool(IsPercentage)}," +
$"{EncodeFloat(Position.x)},{EncodeFloat(Position.y)}," +
$"{EncodeFloat(Color.r)},{EncodeFloat(Color.g)},{EncodeFloat(Color.b)}," +
$"{EncodeBool(Outline)},{EncodeFloat(OutlineOpacity)},{EncodeInt(OutlineThickness)}," +
$"{EncodeBool(CenterDot)},{EncodeFloat(CenterDotOpacity)},{EncodeInt(CenterDotThickness)}," +
$"{EncodeBool(ShowInnerLines)},{EncodeFloat(InnerLineOpacity)},{EncodeInt(InnerLineLenght)}," +
$"{EncodeInt(InnerLineThickness)},{EncodeInt(InnerLinesOffset)}," +
$"{EncodeBool(ShowOuterLines)},{EncodeFloat(OuterLineOpacity)},{EncodeInt(OuterLineLenght)}," +
$"{EncodeInt(OuterLineThickness)},{EncodeInt(OuterLineOffset)}";
return parameters;
}
public void DecodeCrosshairParameters(string encodedString)
{
var parameters = encodedString.Split(',');
// Convert "1" or "0" back to boolean
bool DecodeBool(string value) => value == "1";
// Convert string back to float
float DecodeFloat(string value) => float.Parse(value);
// Convert string back to int
int DecodeInt(string value) => int.Parse(value);
// Assign the decoded values back to the properties
IsPercentage = DecodeBool(parameters[0]);
Position = new Vector2(DecodeFloat(parameters[1]), DecodeFloat(parameters[2]));
Color = new Color(DecodeFloat(parameters[3]), DecodeFloat(parameters[4]), DecodeFloat(parameters[5]));
Outline = DecodeBool(parameters[6]);
OutlineOpacity = DecodeFloat(parameters[7]);
OutlineThickness = DecodeInt(parameters[8]);
CenterDot = DecodeBool(parameters[9]);
CenterDotOpacity = DecodeFloat(parameters[10]);
CenterDotThickness = DecodeInt(parameters[11]);
ShowInnerLines = DecodeBool(parameters[12]);
InnerLineOpacity = DecodeFloat(parameters[13]);
InnerLineLenght = DecodeInt(parameters[14]);
InnerLineThickness = DecodeInt(parameters[15]);
InnerLinesOffset = DecodeInt(parameters[16]);
ShowOuterLines = DecodeBool(parameters[17]);
OuterLineOpacity = DecodeFloat(parameters[18]);
OuterLineLenght = DecodeInt(parameters[19]);
OuterLineThickness = DecodeInt(parameters[20]);
OuterLineOffset = DecodeInt(parameters[21]);
}
protected override void OnStart()
{
if (Code == null)
{
Code = EncodeCrosshairParameters();
}
DecodeCrosshairParameters(Code);
_cachedCode = Code;
}
protected override void OnUpdate()
{
if (Code != _cachedCode)
{
DecodeCrosshairParameters(Code);
}
Code = EncodeCrosshairParameters();
_cachedCode = Code;
}
/// <summary>
/// the hash determines if the system should be rebuilt. If it changes, it will be rebuilt
/// </summary>
protected override int BuildHash()
{
var positionHash = System.HashCode.Combine(
IsPercentage,
Position
);
var crosshairHash = System.HashCode.Combine(
Color,
Outline,
OutlineOpacity,
OutlineThickness,
CenterDot,
CenterDotOpacity,
CenterDotThickness
);
var innerLinesHash = System.HashCode.Combine(
ShowInnerLines,
InnerLineOpacity,
InnerLineLenght,
InnerLineThickness,
InnerLinesOffset
);
var outerLinesHash = System.HashCode.Combine(
ShowOuterLines,
OuterLineOpacity,
OuterLineLenght,
OuterLineThickness,
OuterLineOffset
);
var codeHash = Code.GetHashCode();
return System.HashCode.Combine(
positionHash,
crosshairHash,
innerLinesHash,
outerLinesHash,
codeHash
);
}
}
@inherits PanelComponent
<root> <div class="ynal2p"> @t </div> </root>
@code
{
[Property] public game g {get;set;}
string t = "You need at least 2 players.";
protected override void OnUpdate()
{
if (g.IsActive && g.Players.Count >= 2)
d(this);
}
[Rpc.Broadcast]
public void d(PanelComponent pc)
{pc.Destroy();}
}@using Skateboard.Player
@using Skateboard.Tricks
@inherits PanelComponent
<root>
<div class="@GetMultiplierClass()">@_multiplierText</div>
<div class="@GetTrickClass()">@_trickText</div>
</root>
@code
{
private float _displayTime;
private const float MaxDisplayTime = 5f;
private bool _lastFinished;
private int _pulseTick;
private string _multiplierText = "";
private string _trickText = "";
private bool _done;
private bool _failed;
private bool _fadeout;
private bool _visuallyEmpty;
protected override void OnStart()
{
TrickScoreHolder.OnLocalTrickScoreUpdate += OnTrickScoreUpdate;
}
protected override void OnDestroy()
{
TrickScoreHolder.OnLocalTrickScoreUpdate -= OnTrickScoreUpdate;
}
private void OnTrickScoreUpdate()
{
_displayTime = 0f;
_pulseTick++;
StateHasChanged();
}
protected override void OnUpdate()
{
var holder = SkatePawn.Local?.TrickScores;
if ( holder is null )
return;
if ( _displayTime < MaxDisplayTime && holder.Finished )
_displayTime += Time.Delta;
if ( _lastFinished != holder.Finished )
{
_lastFinished = holder.Finished;
_displayTime = 0f;
}
var visuallyEmpty = holder.VisuallyEmpty;
var multiplierText = visuallyEmpty ? "" : $"{holder.Score} x {holder.Multiplier}";
var trickText = visuallyEmpty ? "" : holder.String;
var done = holder.Finished;
var failed = holder.Failed;
var fadeout = _displayTime >= MaxDisplayTime;
if ( _visuallyEmpty == visuallyEmpty &&
_multiplierText == multiplierText &&
_trickText == trickText &&
_done == done &&
_failed == failed &&
_fadeout == fadeout )
return;
_visuallyEmpty = visuallyEmpty;
_multiplierText = multiplierText;
_trickText = trickText;
_done = done;
_failed = failed;
_fadeout = fadeout;
StateHasChanged();
}
private string GetMultiplierClass()
{
return BuildClass( $"multiplier skate-trick-text {GetPulseClass()}" );
}
private string GetTrickClass()
{
return BuildClass( $"tricks skate-trick-text {GetPulseClass()}" );
}
private string BuildClass( string baseClass )
{
if ( _visuallyEmpty )
return $"{baseClass} hidden";
if ( _fadeout )
baseClass += " fadeout";
if ( _failed )
baseClass += " failed";
else if ( _done )
baseClass += " done";
return baseClass;
}
private string GetPulseClass()
{
return _pulseTick % 2 == 0 ? "pulse pulse-a" : "pulse pulse-b";
}
}
@inherits PanelComponent
<root> <div class="eyelids" style="opacity: @o;"> </div> </root>
@code
{
public float o {get;set;} = 0;
protected override int BuildHash() => System.HashCode.Combine(o);
}@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("PerkChoicePanel.razor.scss")]
@{
var player = GetViewedPlayer();
var isReadOnly = IsReadOnly( player );
var showViewingLabel = isReadOnly && !Manager.Instance.IsSpectator;
if ( !player.IsValid() || (!player.IsChoosingLevelUpReward && !showViewingLabel) )
{
Manager.Instance.IsHoveringPerkChoicePanel = false;
return;
}
}
@{
var showBanish = player.HasGottenBanish;
var showSkip = GetStat( player, PlayerStat.SkipPerkNumRerolls ) > 0f && !player.IsBeingShownCurseChoices;
var showRandom = GetStat( player, PlayerStat.CanChooseRandomPerk ) > 0f;
int numChoices = player.GetDisplayedPerkChoiceCount();
}
<root style="bottom: @(Manager.Instance.ShowBossHealthbar ? 50 : 10)px;">
@if ( showViewingLabel )
{
<div class="viewing_label">
<label>VIEWING CHOICES OF</label>
<div class="viewing_avatar" style="background-image: url( avatar:@player.Network.Owner.SteamId );"></div>
</div>
}
@if ( player.IsChoosingLevelUpReward )
{
@if ( player.NumPerkPointsAvailable > (player.IsBeingShownCurseChoices ? 0 : 1) )
{
@{
var left = 33 + (showBanish ? -8 : 0) + (showSkip ? -6 : 0) + (showRandom ? -5 : 0);
}
<div class="num_left" style="left:@(left)%;">
@{ var extraPerks = player.NumPerkPointsAvailable - 1; }
<label style="color: #ffffffbb; padding-right: 2px;">@($"{extraPerks}")</label>
<label style="color: #ffffff88; padding-left: 2px;">@($"MORE {(extraPerks > 1 ? "PERKS" : "PERK")}")</label>
</div>
}
<div class="itemselection @(numChoices > 17 ? "scrollable" : "")" style="gap: @(numChoices == 5 ? 6 : 12)px;">
@{
bool onlyShowIcons = numChoices > 5;
}
@for ( int i = 0; i < player.GetDisplayedPerkChoiceCount(); i++ )
{
var perkType = player.GetDisplayedPerkChoice( i );
var isUnknown = player.GetDisplayedPerkChoiceUnknown( i );
var index = i;
<PerkChoice ViewedPlayer=@player PerkType=@perkType IsChoice=@true OnlyShowIcon=@onlyShowIcons Slot=@i IsUnknown=@isUnknown onclick="@(e => PerkChosen( e, perkType, index ))" />
}
@if ( GetStat( player, PlayerStat.ChooseTimeLimit ) > 0f )
{
<div class="timelimitbar">
@{
var timeLimitProgress = Utils.Map( player.RealTimeSinceOfferedChoices, 0f, GetStat( player, PlayerStat.ChooseTimeLimit ), 0f, 1f );
}
<div style="width:@((1f - timeLimitProgress) * 100f)%; background-color:@(Color.Lerp(new Color(1f, 0.8f, 0.8f), new Color(1f, 0f, 0f), timeLimitProgress).Rgba);"></div>
</div>
}
</div>
<div class="lower_row">
<div class="button_outer">
@{
bool canUseArmor = GetStat( player, PlayerStat.ArmorRerollCost ) > 0f && player.Armor >= (int)GetStat( player, PlayerStat.ArmorRerollCost );
bool isFree = GetStat( player, PlayerStat.FreeRerollTime ) > 0f && player.RealTimeSinceLvlUp < GetStat( player, PlayerStat.FreeRerollTime );
bool canReroll = player.NumRerollAvailable > 0 || isFree || canUseArmor;
var autoRerollTimerProgress = player.GetDisplayedAutoRerollTimerProgress();
}
<panel class="button_container @((canReroll && !isReadOnly) ? "" : "disabled")" style="background-image: url(@("/textures/ui/panel/reroll_panel.png"));" onclick="@(e => RerollClicked( e ))">
@{
string amountText = isFree ? "∞" : (canUseArmor ? $"{(int)GetStat(player, PlayerStat.ArmorRerollCost)}⛊" : $"{player.NumRerollAvailable}");
int amountFontSize = canUseArmor ? 16 : 20;
}
<InputHint class="ctrl" Button="R" style="opacity: @((canReroll && !isReadOnly) ? 1f : 0.08f);"></InputHint>
<label class="button" style="color:@((canReroll && !isReadOnly) ? "#E8FFF4CC" : "#ffffff99");"></label>
<label class="amount" style="color:@((canReroll && !isReadOnly) ? "#88B19E" : "#ffffff99"); font-size:@($"{amountFontSize}px");">@amountText</label>
@if ( isFree )
{
@{
var freeRerollProgress = Utils.Map( player.RealTimeSinceLvlUp, 0f, GetStat( player, PlayerStat.FreeRerollTime ), 0f, 1f );
}
<div class="free_reroll_bar">
<div style="width:@((1f - freeRerollProgress) * 100f)%;"></div>
</div>
}
@if ( autoRerollTimerProgress > 0f && canReroll )
{
<div class="autoreroll_bar">
<div style="width:@((1f - autoRerollTimerProgress) * 100f)%;"></div>
</div>
}
</panel>
</div>
@if ( showBanish )
{
<div class="button_outer">
<panel class="button_container @((player.NumBanishAvailable > 0 && !isReadOnly) ? "" : "disabled")" style="background-image: url(@("/textures/ui/panel/banish_panel.png"));" onclick="@(e => BanishClicked( e ))">
<InputHint class="ctrl" Button="banish" style="opacity: @((player.NumBanishAvailable > 0 && !isReadOnly) ? 1f : 0.08f);"></InputHint>
<label class="button" style="color:@((player.NumBanishAvailable > 0 && !isReadOnly) ? "#E8FFF4CC" : "#ffffff99");"></label>
<label class="amount" style="color:@((player.NumBanishAvailable > 0 && !isReadOnly) ? "#88B19E" : "#ffffff99");">@player.NumBanishAvailable</label>
</panel>
</div>
}
@if ( showSkip )
{
<div class="button_outer">
<panel class="button_container @(isReadOnly ? "disabled" : "")" style="background-image: url(@("/textures/ui/panel/skip_panel.png"));" onclick="@(e => SkipClicked( e ))">
<label class="button" style="color:@(isReadOnly ? "#ffffff99" : "#E8FFF4CC"); width: 90px;"></label>
<label class="amount" style="font-size: 16px; color:@(!isReadOnly && (int)GetStat(player, PlayerStat.SkipPerkNumRerolls) > 0 ? "#88B19E" : "#ffffff55");">@($"+{(int)GetStat(player, PlayerStat.SkipPerkNumRerolls)}")</label>
</panel>
</div>
}
@if ( showRandom )
{
<div class="button_outer">
<panel class="button_container @(isReadOnly ? "disabled" : "")" style="background-image: url(@("/textures/ui/panel/random_panel.png"));" onclick="@(e => RandomClicked( e ))">
<label class="button" style="color:@(isReadOnly ? "#ffffff99" : "#E8FFF4CC"); width: 120px;"></label>
</panel>
</div>
}
</div>
}
</root>
@code {
private Player GetViewedPlayer()
{
var manager = Manager.Instance;
var player = manager.SelectedPlayer.IsValid()
? manager.SelectedPlayer
: manager.LocalPlayer;
if ( player.IsValid() && player.IsDead )
return null;
return player;
}
private bool IsReadOnly( Player player )
{
return player.IsValid() && player != Manager.Instance.LocalPlayer;
}
private Player GetInteractivePlayer()
{
var player = GetViewedPlayer();
return IsReadOnly( player ) ? null : player;
}
private float GetStat( Player player, PlayerStat stat )
{
return player.IsValid() ? player.GetUiStat( stat ) : 0f;
}
protected override void OnMouseOver( MousePanelEvent e )
{
Manager.Instance.IsHoveringPerkChoicePanel = true;
}
protected override void OnMouseOut( MousePanelEvent e )
{
Manager.Instance.IsHoveringPerkChoicePanel = false;
}
protected void PerkChosen( PanelEvent e, TypeDescription type, int index )
{
e.StopPropagation();
if ( Manager.Instance.IsGameOver || Manager.Instance.IsPaused )
return;
var player = GetInteractivePlayer();
if ( !player.IsValid() )
return;
if ( Manager.Instance.HoveredPerkType != null && Manager.Instance.IsHoveredPerkAChoice )
{
Manager.Instance.HoveredPerkType = null;
Manager.Instance.HoveredPerkViewedPlayer = null;
Manager.Instance.HoveredPerkChoiceSlot = -1;
}
if ( !player.CanInteractWithChoices )
return;
if ( player.IsBanishMode )
{
player.CanInteractWithChoices = false;
player.BanishPerkUIChoice( type );
Manager.Instance.PlaySfxUI( "banish_perk", pitch: Utils.Map( player.NumBanishAvailable, 0, 5, 1f, 1.3f, EasingType.QuadIn ), volume: 0.95f );
}
else
{
player.CanInteractWithChoices = false;
player.AddPerkUIChoice( type );
Manager.Instance.PlaySfxUI( "click", pitch: Utils.Map( player.NumPerkPointsAvailable, 0, 10, 1f, 0.8f, EasingType.QuadIn ), volume: 0.75f );
}
}
protected void RerollClicked( PanelEvent e )
{
e.StopPropagation();
var player = GetInteractivePlayer();
if ( !player.IsValid() )
return;
player.UseReroll();
}
protected void BanishClicked( PanelEvent e )
{
e.StopPropagation();
var player = GetInteractivePlayer();
if ( !player.IsValid() )
return;
player.ToggleBanish();
}
protected void SkipClicked( PanelEvent e )
{
e.StopPropagation();
var player = GetInteractivePlayer();
if ( !player.IsValid() )
return;
player.RefreshAfterChoosingPerk();
var perkType = TypeLibrary.GetType( typeof( PerkSkipChoices ) );
if ( player.HasPerk( perkType ) )
{
var skipPerk = player.GetPerk( perkType ) as PerkSkipChoices;
if ( skipPerk != null )
skipPerk.OnSkip();
}
}
protected void RandomClicked( PanelEvent e )
{
e.StopPropagation();
var player = GetInteractivePlayer();
if ( !player.IsValid() )
return;
var perkType = TypeLibrary.GetType( typeof( PerkRandomChoice ) );
if ( player.HasPerk( perkType ) )
{
var randomPerk = player.GetPerk( perkType ) as PerkRandomChoice;
if ( randomPerk != null )
randomPerk.OnRandom();
}
}
protected override int BuildHash()
{
var player = GetViewedPlayer();
if ( player is null || !player.IsValid() )
return 0;
bool canUseArmor = GetStat( player, PlayerStat.ArmorRerollCost ) > 0f && player.Armor >= (int)GetStat( player, PlayerStat.ArmorRerollCost );
bool isFree = GetStat( player, PlayerStat.FreeRerollTime ) > 0f && player.RealTimeSinceLvlUp < GetStat( player, PlayerStat.FreeRerollTime );
var timeLimitHash = GetStat( player, PlayerStat.ChooseTimeLimit ) > 0f ? player.RealTimeSinceOfferedChoices.Relative : 0f;
var freeRerollHash = isFree ? player.RealTimeSinceLvlUp.Relative : 0f;
var autoRerollTimerHash = player.GetDisplayedAutoRerollTimerProgress();
var lockBanishHash = HashCode.Combine(
player.NumBanishAvailable,
player.HasGottenBanish
);
int buttonsHash = HashCode.Combine(
player.NumRerollAvailable,
canUseArmor,
player.Armor,
isFree,
(int)GetStat( player, PlayerStat.SkipPerkNumRerolls ),
(int)GetStat( player, PlayerStat.CanChooseRandomPerk )
);
var panelStateHash = HashCode.Combine(
player.IsChoosingLevelUpReward,
player.NumPerkPointsAvailable,
player.GetDisplayedPerkChoiceCount(),
player.PerkChoiceHash,
player.IsBeingShownCurseChoices,
IsReadOnly( player )
);
return HashCode.Combine(
timeLimitHash,
freeRerollHash,
lockBanishHash,
buttonsHash,
panelStateHash,
HashCode.Combine(
autoRerollTimerHash,
Manager.Instance.ShowBossHealthbar,
Input.UsingController
)
);
}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("PerkIcon.razor.scss")]
@{
var player = Manager.Instance.LocalPlayer;
var rot = Angle + Perk.IconAngleOffset;
var scale = Perk.IconScale;
const float levelUpAnimDuration = 0.5f;
var levelUpScale = Perk.RealTimeSinceLevelUp < levelUpAnimDuration
? Utils.Map( Perk.RealTimeSinceLevelUp, 0f, levelUpAnimDuration, 1.75f, 1.0f, EasingType.QuadOut )
: 1.0f;
}
<root style="transform: rotate(@(rot)deg) scale(@(scale * levelUpScale));">
@{
// float opacity = (SS2Game.Current.IsGameOver || SS2Game.Current.SelectedPlayer != null) ? 4f : Utils.Map(Item.TimeSinceLevelUp, 0f, 3f, 4f, 1f);
float opacity = Utils.Map( Math.Min( Perk.RealTimeSinceLevelUp, Perk.RealTimeSinceLevelDown ), 0f, 3f, 4f, 1f);
<!-- todo: cache these -->
var attrib = TypeLibrary.GetType(Perk.GetType()).GetAttribute<PerkAttribute>();
var rarity = attrib.Rarity;
var curse = attrib.Curse;
var highlightColor = PerkManager.GetCardRarityColor(rarity, curse, alpha: 0.7f);
var rarityBgTint = PerkManager.GetCardRarityColor(rarity, curse);
var width = Math.Clamp(Perk.Level / (float)Perk.MaxLevel, 0f, 1f) * 100f;
var maxLevel = PerkManager.GetMaxLevelForRarity(rarity);
}
<panel class="rarity" style="opacity: @opacity; background-color: @highlightColor.Rgba; background-image-tint:@rarityBgTint.Rgba;"></panel>
@if (!curse && maxLevel > 1)
{
// @if (Perk.Level > (IsChoice ? 1 : 0))
// {
<panel class="progress" style="width:100%; background-color:@(new Color(0f, 0f, 0f, IsChoice ? 0.3f : 0.6f).Rgba); z-index: 1;"></panel>
// }
<panel class="progress" style="width:@(width)%; background-color:@highlightColor.Rgba; z-index: 2;"></panel>
}
<panel class="icon" style="background-image: url(@(Perk.GetImagePath( Perk.GetType() ))); background-color:@rarityBgTint.Rgba; opacity: @opacity;" />
@if (Perk.Level > Perk.MaxLevel)
{
<div class="maxed">X</div>
}
@if ( !IsChoice && Perk.DisplayCooldown > 0f) // && !Manager.Instance.IsGameOver
{
<div class="cooldown" style="width:@(Math.Clamp(Perk.DisplayCooldown, 0f, 1f) * 100f)%; opacity:@(Utils.Map(Parent.Opacity, 0.3f, 1f, 1.3f, 0.75f)); background-color:@(Perk.DisplayCooldownColor.Rgba);"></div>
}
@if ( !string.IsNullOrEmpty(Perk.DisplayText)) //&& !Manager.Instance.IsGameOver && SS2Game.Current.SelectedPlayer == null)
{
<label class="display_text" style="color:@(Perk.DisplayTextColor.Rgba); opacity:@Perk.DisplayTextOpacity">@Perk.DisplayText</label>
}
@if (Banished)
{
<div class="banish">X</div>
}
@if (Perk.RealTimeSinceHighlight < Perk.HighlightDuration)
{
<div class="highlight" style="opacity:@(Utils.Map( Perk.RealTimeSinceHighlight, 0f, Perk.HighlightDuration, Perk.HighlightOpacity, 0f, EasingType.SineOut)); background-color:@(Perk.HighlightColor.Rgba);"></div>
}
@if(Perk.RealTimeSinceLevelDown < 3f)
{
<div class="deleveled" style="opacity:@(Utils.Map( Perk.RealTimeSinceLevelDown, 0f, 3f, 4f, 0f, EasingType.QuadOut));"></div>
}
</root>
@code
{
public Perk Perk { get; set; }
public bool NoTips { get; set; } = false;
public bool Banished;
public float Angle { get; set; }
public float Scale { get; set; }
public bool IsChoice { get; set; }
protected override void OnMouseOver(MousePanelEvent e)
{
if (e.Target != this || NoTips || Perk == null)
return;
Manager.Instance.HoveredPerkType = TypeLibrary.GetType( Perk.GetType() );
Manager.Instance.HoveredPerkPanel = this;
Manager.Instance.HoveredPerkLevel = Perk.Level;
Manager.Instance.IsHoveredPerkBanished = Banished;
Manager.Instance.IsHoveredPerkAChoice = IsChoice;
Manager.Instance.IsHoveredPerkHidden = false;
Manager.Instance.HoveredPlayer = null;
Manager.Instance.HoveredPlayerIcon = null;
}
protected override void OnMouseOut(MousePanelEvent e)
{
if ( Manager.Instance.HoveredPerkPanel != this ) return;
Manager.Instance.HoveredPerkType = null;
Manager.Instance.HoveredPerkPanel = null;
Manager.Instance.IsHoveredPerkBanished = false;
}
protected override void OnAfterTreeRender ( bool firstTime )
{
base.OnAfterTreeRender( firstTime );
if ( !IsVisible && Manager.Instance.HoveredPerkPanel == this )
{
Manager.Instance.HoveredPerkType = null;
Manager.Instance.HoveredPerkPanel = null;
Manager.Instance.IsHoveredPerkBanished = false;
}
}
protected override int BuildHash()
{
var fadeHash = Math.Min(Perk.RealTimeSinceLevelUp, Perk.RealTimeSinceLevelDown) < 3f ? RealTime.Now : 0f;
var highlightHash = Perk.RealTimeSinceHighlight.Relative < Perk.HighlightDuration ? Perk.RealTimeSinceHighlight.Relative : 0f;
var cooldownHash = Perk.DisplayCooldown > 0f ? HashCode.Combine(Perk.DisplayCooldown, Parent.Opacity, Perk.DisplayCooldownColor) : 0f;
var textHash = !string.IsNullOrEmpty(Perk.DisplayText) ? HashCode.Combine(Perk.DisplayText, Perk.DisplayTextColor, Perk.DisplayTextOpacity) : 0f;
var transformHash = HashCode.Combine(Angle, Perk.IconAngleOffset, Perk.IconScale);
return HashCode.Combine(
Perk.Level,
fadeHash,
textHash,
Manager.Instance.IsGameOver,
cooldownHash,
highlightHash,
transformHash
);
}
protected override void OnClick(MousePanelEvent e)
{
// if non-local player is selected, do nothing
if (Manager.Instance.SelectedPlayer.IsValid())
return;
var player = Manager.Instance.LocalPlayer;
if ( !player.IsValid() )
return;
if( player.Stats[PlayerStat.ClickAddPerk] > 0f )
{
var perkType = TypeLibrary.GetType( typeof( PerkClickAdd ) );
if ( player.HasPerk( perkType ) )
{
var perk = player.GetPerk( perkType ) as PerkClickAdd;
if ( perk != null )
{
perk.Activate(TypeLibrary.GetType(Perk.GetType()));
Manager.Instance.HoveredPerkType = null;
Manager.Instance.IsHoveredPerkBanished = false;
e.StopPropagation();
}
}
}
}
protected override void OnRightClick(MousePanelEvent e)
{
// if non-local player is selected, do nothing
if (Manager.Instance.SelectedPlayer.IsValid())
return;
var player = Manager.Instance.LocalPlayer;
if ( !player.IsValid() )
return;
if( player.Stats[PlayerStat.ClickRemovePerk] > 0f )
{
var perkType = TypeLibrary.GetType( typeof( PerkClickRemove ) );
if ( player.HasPerk( perkType ) )
{
var perk = player.GetPerk( perkType ) as PerkClickRemove;
if ( perk != null )
{
perk.Activate(TypeLibrary.GetType(Perk.GetType()));
Manager.Instance.HoveredPerkType = null;
Manager.Instance.IsHoveredPerkBanished = false;
e.StopPropagation();
}
}
}
// player.LevelDownPerk(TypeLibrary.GetType(Perk.GetType()));
}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits PanelComponent
@attribute [StyleSheet("PerkWorldPanel.razor.scss")]
@if(PerkItem.ShouldHidePanel)
{
return;
}
<root>
@{
var bgColor = PerkManager.GetCardRarityColor(PerkItem.PerkRarity, PerkItem.Curse);
}
<panel class="rarity" style="background-color:@(bgColor.Rgba);">
<panel class="icon" style="background-image: url(@(PerkItem.IconPath)); " />
</panel>
</root>
@code
{
[Property] public PerkItem PerkItem { get; set; }
protected override int BuildHash()
{
return HashCode.Combine(Time.Now);
}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("PlayerIcon.razor.scss")]
@{
var borderColor = Manager.Instance.SelectedPlayer.IsValid()
? Color.Lerp(Color.White, new Color(0.5f, 0.5f, 1f), Utils.FastSin(Time.Now * 24f))
: Color.White;
}
<root style="border: @(Player == Manager.Instance.SelectedPlayer ? 1 : 0)px solid @(borderColor.WithAlpha(0.75f).Rgba);">
<div class="icon" style="opacity:@((Player == Manager.Instance.SelectedPlayer ? 1f : 0.5f) * (Player.IsDead ? 0.1f : 1f) * (IsHovering ? 1f : 0.8f));">
<img src="avatar:@Player.Network.Owner.SteamId" />
<div class="hp_bg"></div>
<div class="hp_delta" style="width:@((Player.Health / Player.GetSyncStat(PlayerStat.MaxHp)) * 100f)%;"></div>
<div class="hp_fill" style="width:@((Player.Health / Player.GetSyncStat(PlayerStat.MaxHp)) * 100f)%;"></div>
</div>
<div class="player_level">@(Player.Level)</div>
</root>
@code
{
public Player Player { get; set; }
public bool IsHovering;
protected override void OnClick(MousePanelEvent e)
{
base.OnClick(e);
if (Manager.Instance.SelectedPlayer == Player)
Manager.Instance.SelectedPlayer = null;
else
Manager.Instance.SelectedPlayer = Player;
e.StopPropagation();
}
protected override void OnMouseOver(MousePanelEvent e)
{
if(e.Target != this)
return;
Manager.Instance.HoveredPlayerIcon = Player;
IsHovering = true;
}
protected override void OnMouseOut(MousePanelEvent e)
{
Manager.Instance.HoveredPlayerIcon = null;
IsHovering = false;
}
protected override int BuildHash()
{
return HashCode.Combine(RealTime.Now);
}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Text.Json;
@inherits Panel
@attribute [StyleSheet("PlayerProfilePanel.razor.scss")]
<root>
<div class="hide_button" onclick=@(() => HideProfile())></div>
@if(PlayerStats is null)
{
@* <div class="loading">
<label>@($"Loading...")</label>
</div> *@
return;
}
<div class="name_container">
<div class="avatar" style="background-image: url( avatar:@Manager.Instance.PlayerProfileToShow.steamId )"></div>
@{
var displayName = Manager.Instance.PlayerProfileToShow.displayName;
}
<div class="displayName">@displayName</div>
</div>
<div class="main-content">
@* @if( _isLoadingPerks)
{
<div class="loading_perks">@($"Loading perks...")</div>
}
else
{
if(_perkPickPercents.Count > 0)
{
<div class="perks_container">
<div class="perks_title">@($"Favorite Perks:")</div>
<div class="perks">
@{
foreach(var pair in _perkPickPercents.OrderByDescending(x => x.Value).Take(10))
{
var perkType = pair.Key;
<PerkIconStatic style="height: 100%;" PerkType=@perkType Level=@(1) HideLevel=@true [email protected] />
}
}
</div>
</div>
}
} *@
@* <div class="stats_title">
Stats
</div> *@
<div class="stats">
<DifficultyPanel DontChangeGameDifficulty=@true />
@{
var numRuns = GetStatSum(StatType.NumRuns);
var numWins = GetStatSum(StatType.NumVictory);
// var numLosses = GetStatSum(StatType.NumDefeat);
// var numResets = Math.Max(numRuns - (numWins + numLosses), 0);
var winPercent = numRuns > 0 ? (numWins / (float)numRuns) * 100f : 0f;
string winRateString;
if(winPercent >= 1f) winRateString = $"{winPercent:0.#}%";
else if(winPercent >= 0.1f) winRateString = $"{winPercent:0.##}%";
else if(winPercent >= 0.01f) winRateString = $"{winPercent:0.###}%";
else if(winPercent > 0f) winRateString = $"{winPercent:0.####}%";
else winRateString = "0%";
var numKills = GetStatSum(StatType.NumKills);
var numMinibossKills = GetStatSum(StatType.NumMinibossKills);
var fastestWinScore = GetStatMax(StatType.LeaderboardRun);
var hasFastestWin = fastestWinScore > Manager.VICTORY_OFFSET / 2f;
var fastestWinTime = hasFastestWin ? Manager.VICTORY_OFFSET - fastestWinScore : 0f;
var fastestWinString = hasFastestWin ? Utils.FormatTime(fastestWinTime) : "...";
var i = 0;
PlayerStatsToShow.Clear();
PlayerStatsToShow.Add(new PlayerProfileStatData("Victories", numWins.ToString("N0"), new Color(1f, 1f, 0.5f), "win", GetFontSizeForNumber(numWins)));
PlayerStatsToShow.Add(new PlayerProfileStatData("Fastest Victory", fastestWinString, new Color(0.5f, 1f, 0.5f), "clock"));
PlayerStatsToShow.Add(new PlayerProfileStatData("Attempts", numRuns.ToString("N0"), new Color(0.8f), "num_runs", GetFontSizeForNumber(numRuns)));
PlayerStatsToShow.Add(new PlayerProfileStatData("Winrate", winRateString, new Color(0.6f, 0.6f, 0.9f, 0.7f), "win_rate"));
PlayerStatsToShow.Add(new PlayerProfileStatData("Enemy Kills", numKills.ToString("N0"), new Color(0.8f, 0.2f, 0.2f), "kill", GetFontSizeForNumber(numKills)));
PlayerStatsToShow.Add(new PlayerProfileStatData("Miniboss Kills", numMinibossKills.ToString("N0"), new Color(0.9f, 0.4f, 0.1f), "kill_miniboss", GetFontSizeForNumber(numMinibossKills)));
}
<div class="list_container">
@foreach(var data in PlayerStatsToShow)
{
<div class="flat list" style="background-color: @((i % 2 == 0 ? new Color(0, 0, 0, 0.4f) : new Color(0, 0, 0, 0.7f)).Rgba);">
<div>
<div class="icon_container">
<label class="icon" style="background-color:@(data.color.Rgba); mask-image:@($"url(/textures/ui/stats/{data.icon}.png)")"></label>
</div>
<label class="bold stat_name" style="font-size:@(data.title.Length > 15 ? Math.Round(Utils.Map(data.title.Length, 15, 36, 14f, 10f, EasingType.SineIn)) : 14)px; color:@(Color.Lerp(data.color, Color.White, (i % 2 == 0 ? 0.5f : 0.4f)).Rgba);">@(data.title)</label>
</div>
<div class="values">
@{
var color = (data.text == "0" || data.text == "0%" || data.text == "...")
? Color.Lerp(data.color, new Color(0.5f), 0.75f).WithAlpha(0.5f).Rgba
: data.color.Rgba;
}
<label class="bold stat_value" style="color:@(color); font-size:@(data.fontSize)px;">@(data.text)</label>
</div>
</div>
@{
i++;
}
}
</div>
</div>
</div>
</root>
@code
{
public struct PlayerProfileStatData
{
public string title;
public string text;
public Color color;
public string icon;
public float fontSize;
public PlayerProfileStatData(string _title, string _text, Color _color, string _icon, float _fontSize = 16f)
{
title = _title;
text = _text;
color = _color;
icon = _icon;
fontSize = _fontSize;
}
}
public List<PlayerProfileStatData> PlayerStatsToShow { get; set; } = new();
private bool _isLoadingPerks;
private long _loadedSteamId;
public Sandbox.Services.Stats.PlayerStats PlayerStats { get; set; }
private Dictionary<TypeDescription, float> _perkPickPercents = new();
protected override void OnAfterTreeRender(bool firstTime)
{
base.OnAfterTreeRender(firstTime);
var currentSteamId = Manager.Instance.PlayerProfileToShow?.steamId ?? 0;
if (firstTime || currentSteamId != _loadedSteamId)
{
_loadedSteamId = currentSteamId;
Refresh();
}
}
async void Refresh()
{
PlayerStats = null;
StateHasChanged();
_isLoadingPerks = true;
PlayerStats = Sandbox.Services.Stats.GetPlayerStats("facepunch.ss2", Manager.Instance.PlayerProfileToShow.steamId);
await PlayerStats.Refresh();
_isLoadingPerks = false;
_perkPickPercents.Clear();
/*
foreach(var type in TypeLibrary.GetTypes<Perk>())
{
var attrib = type.GetAttribute<PerkAttribute>();
if(attrib == null)
continue;
if(attrib.Disabled)
continue;
if(attrib.Curse)
continue;
var timesChosen = (int)PlayerStats.Get(Manager.GetPerkStatString(StatType.PerkChosen, type)).Sum;
var timesIgnored = (int)PlayerStats.Get(Manager.GetPerkStatString(StatType.PerkIgnored, type)).Sum;
// if( timesChosen + timesIgnored > 0 && (timesChosen > 2 || timesIgnored > 3) )
if( timesChosen >= 2 )
{
var percent = MathX.Clamp(timesChosen / (float)(timesChosen + timesIgnored), 0f, 1f);
// Log.Info($"{type}: chosen {timesChosen}, ignored {timesIgnored} - percent: {percent}");
_perkPickPercents[type] = percent;
}
}
*/
// Log.Info($"Loaded {_perkPickPercents.Count} perks for player profile.");
// var totalWinTime = 0f;
// for(int difficulty = 1; difficulty <= Manager.MaxDifficulty; difficulty++)
// {
// }
StateHasChanged();
// var zombies = stats.Get("zombies_killed");
// Log.Info($"Garry has killed {zombies.Sum} zombies!");
// Log.Info($"Refreshed leaderboard with {Leaderboard.Entries.Count()} entries.");
}
protected override int BuildHash()
{
return HashCode.Combine(
DifficultyPanel.DifficultyToDisplay,
Manager.Instance.PlayerProfileToShow
);
}
public void HideProfile()
{
Manager.Instance.PlayerProfileToShow = null;
}
public int GetStatSum( StatType statType )
{
if( DifficultyPanel.DifficultyToDisplay == -1 )
{
int total = 0;
for(int diff = Manager.MinDifficulty; diff <= Manager.MaxDifficulty; diff++)
{
total += (int)PlayerStats.Get(Manager.GetStatString(statType, numPlayers: 1, diff)).Sum;
}
return total;
}
else
{
return (int)PlayerStats.Get(Manager.GetStatString(statType, numPlayers: 1, DifficultyPanel.DifficultyToDisplay)).Sum;
}
}
public float GetStatMax( StatType statType )
{
if( DifficultyPanel.DifficultyToDisplay == -1 )
{
float max = 0f;
for(int diff = Manager.MinDifficulty; diff <= Manager.MaxDifficulty; diff++)
{
var val = (float)PlayerStats.Get(Manager.GetStatString(statType, numPlayers: 1, diff)).Max;
if(val > max)
max = val;
}
return max;
}
else
{
return (float)PlayerStats.Get(Manager.GetStatString(statType, numPlayers: 1, DifficultyPanel.DifficultyToDisplay)).Max;
}
}
public static float GetFontSizeForNumber( float number )
{
if(number >= 1000000000f) return 10f;
if(number >= 100000000f) return 11f;
if(number >= 10000000f) return 12f;
if(number >= 1000000f) return 14f;
return 16f;
}
}
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("PlayerTooltip.razor.scss")]
<root>
@{
string name = "";
}
@if(Player != null )
{
@if(ShowIcon)
{
<div class="icon">
<img src="avatar:@Player.Network.Owner.SteamId" />
<div class="level">@Player.Level</div>
</div>
}
@{
name = Player?.Network.Owner?.DisplayName ?? "";
}
}
else
{
name = Name ?? "";
}
<div class="name">@name</div>
@if(Player != null && Player.Network.Owner != null)
{
<div class="ping">@($"{Player.Network.Owner.Ping:0}ms")</div>
}
</root>
@code {
public Player Player { get; set; }
public bool ShowIcon { get; set; }
public string Name { get; set; }
public override void Tick()
{
SetClass( "hidden", Input.UsingController );
var mousePos = Mouse.Position;
if (mousePos.x > Screen.Width * 0.9f)
{
mousePos += new Vector2(-20f, 20f);
var mousePosRelative = mousePos / Screen.Size;
Style.Right = Length.Fraction(1f - mousePosRelative.x);
Style.Top = Length.Fraction(mousePosRelative.y);
}
else
{
mousePos += new Vector2(20f, 20f);
var mousePosRelative = mousePos / Screen.Size;
Style.Left = Length.Fraction(mousePosRelative.x);
Style.Top = Length.Fraction(mousePosRelative.y);
}
}
protected override int BuildHash()
{
return HashCode.Combine(RealTime.Now);
}
}
@namespace Sandbox
@inherits Panel
@using Sandbox.UI
@implements IRichTextPanel
@attribute [RichTextPanel( @"([+-]?\d+(?:\.\d+)?[%ms]?)[ \t]*(?:→|->)[ \t]*([+-]?\d+(?:\.\d+)?[%ms]?)", UseCaptureGroup = false )]
<root class="richtext-arrow-result">
<label class="val val1" style="color: @(Color.Hex);">@Text1</label>
<label>→</label>
<label class="val val2" style="color: @(Color.Hex);">@Text2</label>
</root>
<style>
.val1
{
opacity: 0.2;
}
</style>
@code
{
public virtual Color Color => Color.Green;
public float? ImageSize { get; set; }
string Text1;
string Text2;
public void ParseRichText(string text)
{
// Log.Info( "Parsing arrow result: " + text );
var split = text.Split( "->");
if(split.Length == 1)
{
split = text.Split( "→" );
}
if(split.Length == 2)
{
Text1 = split[0];
Text2 = split[1];
if ( Text1.StartsWith( "[-]" ) || Text1.StartsWith( "[+]" ) )
{
Text1 = Text1[3..];
}
if ( Text2.EndsWith( "[/-]" ) || Text2.EndsWith( "[/+]" ) )
{
Text2 = Text2[..^4];
}
if(Text1.StartsWith("+") && !Text2.StartsWith("-") && !Text2.StartsWith("+"))
{
Text2 = "+" + Text2;
}
}
else
{
Text1 = text;
Text2 = "";
}
}
protected override int BuildHash()
{
return System.HashCode.Combine( Text1, Text2, Color );
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
<root>
<div class="menu-icon-toggle-group">
<IconPanel Tooltip="Compact" class=@( Size == 60 ? "active" : "" ) Text="view_compact" @onclick=@( () => ChangeSize( 60 ) )></IconPanel>
<IconPanel Tooltip="Medium" class=@( Size == 120 ? "active" : "" ) Text="view_module" @onclick=@( () => ChangeSize( 120 ) )></IconPanel>
<IconPanel Tooltip="Large" class=@( Size == 200 ? "active" : "" ) Text="grid_view" @onclick=@( () => ChangeSize( 200 ) )></IconPanel>
</div>
</root>
@code
{
public int Size { get; set; } = 120;
protected override int BuildHash() => HashCode.Combine( Size );
void ChangeSize( int size )
{
Size = size;
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox
<root>
@if ( Current is not null )
{
var mouse = Mouse.Position;
var left = mouse.x * Panel.ScaleFromScreen - 32;
var top = mouse.y * Panel.ScaleFromScreen - 32;
var iconStyle = string.IsNullOrEmpty( Current.Icon ) ? "" : $"background-image: url({Current.Icon}); ";
<div class="dragging" style="@(iconStyle)left: @(left)px; top: @(top)px;" @ref="DragVisual">
@if ( string.IsNullOrEmpty( Current.Icon ) )
{
<div class="drag-title">@Current.Title</div>
}
</div>
}
</root>
@code
{
public static DragHandler Instance { get; private set; }
/// <summary>
/// The data currently being dragged, or null.
/// </summary>
public static DragData Current { get; private set; }
/// <summary>
/// True if something is actively being dragged.
/// </summary>
public static bool IsDragging => Current is not null;
Panel DragVisual { get; set; }
private RootPanel _rootPanel;
protected override void OnStart()
{
Instance = this;
}
protected override void OnDestroy()
{
if ( Instance == this )
Instance = null;
}
/// <summary>
/// Start dragging with the given data.
/// </summary>
public static void StartDragging( DragData data )
{
Current = data;
data.Source.UserData = data;
Instance?.StateHasChanged();
}
/// <summary>
/// Stop dragging and clear all state.
/// </summary>
public static void StopDragging()
{
if (!IsDragging) return;
Current = null;
Instance?.StateHasChanged();
}
protected override void OnUpdate()
{
if ( !IsDragging ) return;
if ( !_rootPanel.WantsMouseInput() )
{
StopDragging();
return;
}
if ( DragVisual is not null )
{
var mouse = Mouse.Position;
DragVisual.Style.Left = Length.Pixels( mouse.x * Panel.ScaleFromScreen - 32 );
DragVisual.Style.Top = Length.Pixels( mouse.y * Panel.ScaleFromScreen - 32 );
}
}
protected override void OnTreeBuilt()
{
_rootPanel = Panel.FindRootPanel();
}
protected override int BuildHash() => HashCode.Combine( Current );
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
@attribute [SpawnMenuHost.SpawnMenuMode]
@attribute [Icon( "🎨" )]
@attribute [Title( "#spawnmenu.mode.effects" )]
@attribute [Order( -75 )]
<root>
<EffectsList />
<EffectsProperties />
</root>
@using Sandbox;
@using Sandbox.UI;
@namespace Sandbox
@inherits Panel
<root>
<div class="presets-toggle" @onclick=@OnToggleClick>
<i>expand_less</i>
</div>
</root>
@code
{
public PlayerInventory Inventory { get; set; }
void OnToggleClick()
{
var menu = MenuPanel.Open( this );
var presets = PlayerLoadout.GetLoadoutPresets();
foreach ( var preset in presets )
{
var name = preset.Name;
var json = preset.LoadoutJson;
menu.AddSubmenu( "bookmark", name, sub =>
{
sub.AddOption( "play_arrow", "Load", () => OnLoadPreset( json ) );
sub.AddOption( "save", "Overwrite with current", () => OnOverwritePreset( name ) );
sub.AddOption( "close", "Delete", () => OnDeletePreset( name ) );
} );
}
if ( presets.Any() )
{
menu.AddSpacer();
}
menu.AddOption( "refresh", "Reset to Default", ResetToDefault );
menu.AddOption( "add", "New Preset", OnSaveNew );
menu.StateHasChanged();
}
void OnLoadPreset( string json )
{
var loadout = GetLoadout();
if ( !loadout.IsValid() ) return;
loadout.SwitchToPreset( json );
}
void OnOverwritePreset( string name )
{
var json = LocalData.Get<string>( "hotbar" );
if ( string.IsNullOrEmpty( json ) ) return;
PlayerLoadout.SaveLoadoutPreset( name, json );
}
void OnDeletePreset( string name )
{
PlayerLoadout.DeleteLoadoutPreset( name );
}
void ResetToDefault()
{
var loadout = GetLoadout();
if ( !loadout.IsValid() ) return;
loadout.ResetToDefault();
}
void OnSaveNew()
{
var popup = new StringQueryPopup
{
Title = "New Inventoy Preset",
Placeholder = "Enter a name...",
ConfirmLabel = "Save",
OnConfirm = OnSaveConfirmed,
Parent = FindRootPanel()
};
}
void OnSaveConfirmed( string name )
{
var json = LocalData.Get<string>( "hotbar" );
if ( string.IsNullOrEmpty( json ) ) return;
PlayerLoadout.SaveLoadoutPreset( name, json );
}
PlayerLoadout GetLoadout()
{
return Inventory?.GetComponent<PlayerLoadout>();
}
}
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Mounting;
@inherits Panel
@namespace Sandbox
<root>
<div class="title">@Item.Title</div>
<div class="author">
<div class="avatar" style="background-image: url('@Item.Owner.Avatar');"></div>
<div class="name">@Item.Owner.Name</div>
</div>
</root>
@code
{
public Storage.QueryItem Item { get; set; }
public override bool WantsDrag => true;
protected override void OnParametersSet()
{
Style.SetBackgroundImage( Item.Preview );
}
protected override void OnMouseDown( MousePanelEvent e )
{
if ( e.MouseButton == MouseButtons.Right )
{
var menu = MenuPanel.Open( this );
SpawnlistData.PopulateContextMenu( menu, new SpawnlistItem
{
Ident = SpawnlistItem.MakeIdent( "dupe", Item.Id.ToString(), "workshop" ),
Title = Item.Title,
Icon = Item.Preview,
} );
return;
}
base.OnMouseDown( e );
}
protected override async void OnDragStart( DragEvent e )
{
var data = new DragData
{
Type = "dupe",
Icon = Item.Preview,
Title = Item.Title,
Source = this
};
DragHandler.StartDragging( data );
var installed = await Item.Install();
if ( installed is null ) return;
data.Data = installed.Files.ReadAllText( "/dupe.json" );
}
protected override void OnDragEnd(DragEvent e)
{
if ( DragHandler.IsDragging )
{
DragHandler.StopDragging();
}
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
@implements IUtilityTab
@attribute [Icon("🌍")]
@attribute [Title("#spawnmenu.tab.utilities")]
@attribute [Order(0)]
<root class="tab">
<div class="left">
<VerticalMenu class="menuinner">
<Options>
@foreach ( var group in GetVisiblePages().GroupBy( x => x.Group ).OrderBy( x => x.Min( t => t.Order ) ) )
{
@if ( !string.IsNullOrWhiteSpace( group.Key ) )
{
<h2>@group.Key</h2>
}
@foreach ( var type in group.OrderBy( x => x.Order ).ThenBy( x => x.Title ) )
{
<MenuOption Text="@type.Title" Icon="@type.Icon"
class=@( SelectedPageType == type ? "active" : "" )
@onclick="@(() => OnSelect( type ))">
</MenuOption>
}
}
</Options>
</VerticalMenu>
</div>
<div class="body menuinner" @ref="PageContainer"></div>
</root>
@code
{
TypeDescription SelectedPageType { get; set; }
Panel PageContainer;
UtilityPage ActivePage;
IEnumerable<TypeDescription> GetVisiblePages()
{
foreach ( var type in Game.TypeLibrary.GetTypes<UtilityPage>() )
{
if ( type.IsAbstract ) continue;
var instance = type.Create<UtilityPage>();
if ( instance is null || !instance.IsPageVisible() ) continue;
instance.Delete();
yield return type;
}
}
void OnSelect( TypeDescription type )
{
SelectedPageType = type;
ActivePage?.Delete();
ActivePage = type.Create<UtilityPage>();
PageContainer.AddChild( ActivePage );
StateHasChanged();
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox
@if ( !Player.IsValid() || Player.WantsHideHud )
return;
<root>
<div class="vitals">
@if ( Player.Armour > 0 )
{
<div class="stat armour hud-panel">
<Image class="icon" Texture=@ArmourIcon />
<label class="value">@(DisplayArmour)</label>
</div>
}
<div class="stat health hud-panel">
<Image class="icon" Texture=@HealthIcon />
<label class="value">@(DisplayHealth)</label>
</div>
</div>
@if ( Weapon.IsValid() && Weapon.UsesAmmo )
{
<div class="ammo hud-panel">
@if ( WeaponConVars.UnlimitedAmmo )
{
<label class="value">∞</label>
}
else
{
<label class="value">@(Weapon.UsesClips ? Weapon.ClipContents.ToString() : (WeaponConVars.InfiniteReserves ? "∞" : Weapon.ReserveAmmo.ToString()))</label>
@if ( Weapon.UsesClips )
{
<label class="alternate">@(WeaponConVars.InfiniteReserves ? "∞" : Weapon.ReserveAmmo.ToString())</label>
}
}
<Image class="icon" Texture=@AmmoIcon />
</div>
}
</root>
@code
{
[Property] public Texture HealthIcon { get; set; }
[Property] public Texture ArmourIcon { get; set; }
[Property] public Texture AmmoIcon { get; set; }
Player Player => Player.FindLocalPlayer();
BaseWeapon Weapon => Player?.GetComponent<PlayerInventory>()?.ActiveWeapon as BaseWeapon;
int DisplayHealth => (int)Player.Health;
int DisplayArmour => (int)Player.Armour;
bool IsSpawnMenuOpen()
{
var host = Game.ActiveScene.Get<SpawnMenuHost>();
return host?.Panel?.HasClass( "open" ) ?? false;
}
protected override void OnUpdate()
{
Panel.SetClass( "spawnmenu-open", IsSpawnMenuOpen() );
}
protected override int BuildHash()
{
if ( !Player.IsValid() || Player.WantsHideHud ) return 0;
return System.HashCode.Combine( Player.Health, Player.Armour, Weapon?.ClipContents, Weapon?.ReserveAmmo );
}
}
@namespace Facepunch.UI
@inherits PanelComponent
<root>
@foreach (var voice in VoiceList.Where( x => CanDisplay( x ) ) )
{
<div class="item-row">
<div class="avatar-wrap">
<div class="speaking-glow" style="opacity: @GetGlowOpacity( voice ); transform: scale( @GetGlowScale( voice ) )"></div>
<img class="avatar" src="avatar:@voice.Network.Owner.SteamId" />
</div>
<label class="name">@voice.Network.Owner.DisplayName</label>
</div>
}
</root>
@code
{
public IEnumerable<Voice> VoiceList => Scene.GetAllComponents<Voice>();
private Dictionary<ulong, float> _smoothed = new();
private float GetSmoothed( Voice voice )
{
var id = voice.Network.Owner.SteamId;
_smoothed[id] = _smoothed.GetValueOrDefault( id, 0f ).LerpTo( voice.Amplitude, Time.Delta * 10f );
return _smoothed[id];
}
private bool CanDisplay( Voice voice )
{
if ( voice.Network.Owner is null ) return false;
return voice.LastPlayed < 0.25f;
}
private string GetGlowOpacity( Voice voice )
{
var s = GetSmoothed( voice );
return Math.Clamp( s * 4f, 0.2f, 0.9f ).ToString( "0.##" );
}
private string GetGlowScale( Voice voice )
{
var s = GetSmoothed( voice );
return Math.Clamp( 1f + s * 0.6f, 1f, 1.6f ).ToString( "0.##" );
}
protected override int BuildHash()
{
return HashCode.Combine( Time.Now );
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox
<root>
<h2>Hat Selection</h2>
<div class="hatselector">
<div class="image-button" onclick=@Henrietta>
<image src="images/henriettapicture.png" />
</div>
<div class="image-button" onclick=@Watermelon>
<image src="images/melonchickenpic.png" />
</div>
<div class="image-button" onclick=@Shooter>
<image src="images/shooterchickenpic.png" />
</div>
<div class="image-button" onclick=@Tofu>
<image src="images/tofuchickenpic.png" />
</div>
</div>
</root>
<root>
<div class="options">
<div class="button" onclick=@Back>
Back
</div>
</div>
</root>
<root>
<div class="howtoplay">
<p>Press A & D or Left and Right on a gamepad to move.</p>
<p>Press Space or A on a gamepad to jump, double jump, and triple jump.</p>
<p>Press Shift or B on a gamepad to dodge.</p>
</div>
</root>
@code
{
[Property] public SceneFile hen1 { get; set; }
[Property] public SceneFile hen2 { get; set; }
[Property] public SceneFile hen3 { get; set; }
[Property] public SceneFile hen4 { get; set; }
[Property] public SceneFile LB { get; set; }
[Property, TextArea] public string MyStringValue { get; set; } = "Hat Selection";
void Henrietta()
{
Scene.Load(hen1);
}
void Watermelon()
{
Scene.Load(hen2);
}
void Shooter()
{
Scene.Load(hen3);
}
void Tofu()
{
Scene.Load(hen4);
}
void Back()
{
Scene.Load(LB);
}
/// <summary>
/// the hash determines if the system should be rebuilt. If it changes, it will be rebuilt
/// </summary>
protected override int BuildHash() => System.HashCode.Combine( MyStringValue );
}
@using Sandbox
@using Sandbox.UI
@using System.Linq
@inherits PanelComponent
<root>
<div class="board">
<div class="title">@CurrentPageTitle</div>
<div class="page">
@switch (CurrentPage)
{
case PageType.Controls:
<div class="controls-list">
<div class="control-row">
<div class="label">Move</div>
<div class="glyphs">
<Image class="glyph" [email protected]("Forward", InputGlyphSize.Large, GlyphStyle.Light) />
<Image class="glyph" [email protected]("Left", InputGlyphSize.Large, GlyphStyle.Light) />
<Image class="glyph" [email protected]("Backward", InputGlyphSize.Large, GlyphStyle.Light) />
<Image class="glyph" [email protected]("Right", InputGlyphSize.Large, GlyphStyle.Light) />
</div>
</div>
<div class="control-row">
<div class="label">Jump</div>
<div class="glyphs">
<Image class="glyph" [email protected]("Jump", InputGlyphSize.Large, GlyphStyle.Light) />
</div>
</div>
<div class="control-row">
<div class="label">Leap Dive</div>
<div class="glyphs">
<Image class="glyph" [email protected]("Attack1", InputGlyphSize.Large, GlyphStyle.Light) />
</div>
</div>
</div>
break;
case PageType.Title:
<div class="title-page">
<img class="title-thumbnail" src="textures/dropout_thumbnail_910x512.png" />
</div>
break;
}
</div>
</div>
</root>
@code {
// To add a new page: add an enum value, add it to Pages, and add a switch case above.
private enum PageType
{
Controls,
Title,
}
private static readonly PageType[] Pages = new[]
{
PageType.Controls,
PageType.Title,
};
/// <summary>Seconds each page is shown before cycling to the next.</summary>
private const float PageDuration = 8f;
private int _pageIndex = 0;
private TimeUntil _nextPageAt = PageDuration;
private PageType CurrentPage => Pages[_pageIndex];
private string CurrentPageTitle => CurrentPage switch
{
PageType.Controls => "Controls",
PageType.Title => "",
_ => string.Empty,
};
protected override void OnUpdate()
{
if (Pages.Length <= 1) return;
if ((float)_nextPageAt <= 0f)
{
_pageIndex = (_pageIndex + 1) % Pages.Length;
_nextPageAt = PageDuration;
}
}
protected override int BuildHash() => System.HashCode.Combine(_pageIndex, Input.UsingController);
}
@using Sandbox
@using Sandbox.UI
@using System.Linq
@inherits PanelComponent
<root>
<div class="board">
@if (LobbyManager.HasActiveLobbyMessage)
{
<div class="message-banner">@LobbyManager.LobbyMessage</div>
}
@if (LaunchSeconds > 0)
{
<div class="title">Starting in</div>
<div class="launch-number">@LaunchSeconds</div>
}
else
{
<div class="title">Players</div>
<div class="grid">
@foreach (PlayerReadyState player in Scene.GetAllComponents<PlayerReadyState>())
{
<div class="cell @(player.IsReady ? "ready" : "notready")">
@if (player.IsBot())
{
<div class="avatar bot-avatar" style="background-color: @(BotColor(player.DisplayName));">
@(BotInitial(player.DisplayName))
</div>
}
else
{
<img class="avatar" src="avatar:@(player.Network.Owner.SteamId)" />
}
<div class="name">@player.DisplayName</div>
<div class="wins" title="Wins">
<div class="trophy">🏆</div>
<div class="count">@player.WinCount</div>
</div>
</div>
}
</div>
}
</div>
</root>
@code {
private int LaunchSeconds
{
get
{
LobbyManager mgr = LobbyManager.Current;
if (mgr is null || !mgr.IsLaunching) return 0;
return System.Math.Max(1, (int)System.MathF.Ceiling(mgr.LaunchSecondsRemaining));
}
}
private static string BotInitial(string name) =>
string.IsNullOrEmpty(name) ? "?" : name.Substring(0, 1).ToUpper();
// Stable per-name HSL so each bot keeps the same badge color across ticks.
private static string BotColor(string seed)
{
int hue = System.Math.Abs(seed.GetHashCode()) % 360;
return $"hsl({hue}, 55%, 45%)";
}
protected override int BuildHash()
{
IEnumerable<string> parts = Scene.GetAllComponents<PlayerReadyState>()
.Select(p => $"{p.DisplayName}:{p.IsReady}:{p.WinCount}");
return System.HashCode.Combine(string.Join(",", parts), LaunchSeconds, LobbyManager.HasActiveLobbyMessage);
}
}
@namespace CardGames
@using Sandbox
@using Sandbox.UI
@using System
@using System.Linq
@using System.Collections.Generic
@inherits Panel
@*
The full games browser - reached from the launcher's "Browse Games" button (host only).
A search box and sort tabs drive a Package.FindAsync query; results show as cards. Picking one asks the
director to mount its cloud package and start it. Failures (won't download, not a card game) stay here.
*@
<root class="cgui">
<button class="back" onclick=@(() => Director?.CloseBrowser())>‹ Back</button>
<div class="hall">
<div class="controls">
<div class="search">
<span class="ico">search</span>
<TextEntry placeholder="Search games…" OnTextEdited=@OnSearchEdited></TextEntry>
</div>
<div class="sorts">
@foreach ( var s in Sorts )
{
var sort = s;
<button class="sort @(_sort == sort.Key ? "active" : "")" onclick=@(() => SetSort( sort.Key ))>@sort.Value</button>
}
</div>
</div>
@if ( !string.IsNullOrEmpty( _error ) )
{
<div class="cg-error">@_error</div>
}
@if ( _busy )
{
<div class="cg-note">Searching…</div>
}
else if ( _results.Count == 0 )
{
<div class="cg-note">No games found. Try a different search.</div>
}
else
{
<div class="grid">
@foreach ( var package in _results )
{
var p = package;
<GameCard Package=@p OnClick=@(() => Select( p )) Loading=@(_loadingIdent == p.FullIdent) />
}
</div>
}
</div>
</root>
@code
{
private GameDirector _director;
private GameDirector Director => _director ??= Sandbox.Game.ActiveScene?.Get<GameDirector>();
// Sort orders offered as tabs - the key goes straight into the FindAsync query (sort:<key>).
private static readonly KeyValuePair<string, string>[] Sorts =
{
new( "trending", "Trending" ),
new( "popular", "Popular" ),
new( "newest", "Newest" ),
new( "quality", "Top Rated" ),
};
private string _sort = "popular";
private string _error = "";
private bool _busy;
private string _loadingIdent; // the package currently being mounted/started (per-card spinner)
private List<Package> _results = new();
private string _query = "";
private bool _searchPending;
private RealTimeSince _sinceEdited;
private bool _searched; // kick off the first search once, after the panel mounts
private void SetSort( string sort )
{
if ( _sort == sort ) return;
_sort = sort;
Refresh();
}
// Debounce typing: note the edit, then fire the search a beat after the last keystroke (see Tick).
private void OnSearchEdited( string text )
{
_query = text ?? "";
_searchPending = true;
_sinceEdited = 0f;
}
public override void Tick()
{
base.Tick();
if ( !_searched )
{
_searched = true;
Refresh();
}
if ( _searchPending && _sinceEdited > 0.3f )
{
_searchPending = false;
Refresh();
}
}
// Query the cloud for games matching the current sort + search text.
private async void Refresh()
{
_busy = true;
_error = "";
StateHasChanged();
try
{
var query = $"type:cggame sort:{_sort} {_query}".Trim();
var result = await Package.FindAsync( query, 60 );
_results = result.Packages?.ToList() ?? new();
}
catch ( Exception e )
{
_results = new();
_error = "Couldn't reach the games list. Check your connection.";
Log.Warning( e, "Games search failed" );
}
_busy = false;
StateHasChanged();
}
// Mount + start the chosen package. On success the HUD swaps this panel out; on failure we show why.
private async void Select( Package package )
{
if ( _loadingIdent is not null || Director is null ) return;
_loadingIdent = package.FullIdent;
_error = "";
StateHasChanged();
var error = await Director.StartCommunityGame( package );
_loadingIdent = null;
if ( !string.IsNullOrEmpty( error ) )
_error = error;
StateHasChanged();
}
protected override int BuildHash() => HashCode.Combine( _busy, _error, _loadingIdent, _sort, _results.Count );
}
@namespace CardGames
@using Sandbox
@using Sandbox.UI
@using System
@using System.Linq
@inherits Panel
@*
Hosts a SeatTag over each occupied seat's hand. Generic across games - the tags read names, chips and
per-seat badges (CardGame.SeatBadges) and position themselves in the world. This panel just decides
which seats exist; each tag handles its own placement and content.
*@
<root class="players">
@if ( Game is { } game )
{
@foreach ( var s in game.Seats.Where( s => s.Occupied ).OrderBy( s => s.Index ) )
{
var seat = s;
<SeatTag Seat=@seat />
}
}
</root>
@code
{
private CardGame Game => Sandbox.Game.ActiveScene?.Get<GameDirector>()?.ActiveGame;
// Re-list only when the set of occupied seats changes; the tags repaint themselves otherwise.
protected override int BuildHash()
{
if ( Game is not { } game ) return 0;
int h = 0;
foreach ( var s in game.Seats.Where( s => s.Occupied ) )
h = HashCode.Combine( h, s.Index );
return h;
}
}