Search the source of every open source package.
25 results
@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
);
}
}
@using System
@using System.Threading.Tasks
@using Sandbox
@using Sandbox.UI
@inherits WebPanel
<style>
/* .scss files don't work in libraries. https://github.com/Facepunch/sbox-issues/issues/4813 */
prompt {
width: 100%;
pointer-events: all;
}
</style>
@code {
public readonly TaskCompletionSource<bool> Purchased;
public Prompt( string URL )
{
Url = URL;
Purchased = new TaskCompletionSource<bool>();
if ( !Game.ActiveScene.Components.TryGet( out ScreenPanel panel ) )
{
panel = Game.ActiveScene.Components.Create<ScreenPanel>();
panel.GetPanel().ElementName = "monetization";
panel.ZIndex = int.MaxValue;
}
Parent = panel.GetPanel();
}
public override void OnDeleted()
{
if ( !Game.ActiveScene.Components.TryGet( out ScreenPanel panel ) )
{
return;
}
if ( panel.GetPanel().HasChildren )
{
return;
}
panel.Destroy();
}
protected override void OnAfterTreeRender( bool firstTime )
{
// This was the only way I could think of to get the
// Steam browser to send data back data to the game.
// If you have a better way let me know.
switch ( Surface.PageTitle )
{
case "finished": Delete();
break;
case "purchased": Purchased.TrySetResult( true );
break;
case "cancelled": Purchased.TrySetResult( false );
break;
}
}
protected override int BuildHash() => HashCode.Combine( Surface.PageTitle );
}
@using Sandbox;
@using Sandbox.Audio;
@using Sandbox.UI;
@using Sandbox.Services;
@using System;
@using System.Collections.Generic;
@using System.Linq;
@inherits PanelComponent
<root class="menu">
<div class="container">
<div class="title">
<span>Options</span>
</div>
@if (activeTab != null)
{
<div class="content tabs-container">
<div class="tabs-group">
@foreach (var tab in tabs)
{
if (tab == null)
continue;
<div class="button red @(tab == activeTab ? "active" : "inactive")" onclick="@(() => activeTab = tab)">
@tab.tabName
</div>
}
</div>
</div>
<div class="content tab-content" CanDragScroll="false">
<div class="table">
@foreach (var group in activeTab.groups)
{
<div class="row title">
<div class="label">@group.groupName</div>
</div>
foreach (var element in group.elements)
{
var elementType = element.GetType();
<div class="row setting">
<div class="column key">
<div class="label">@element.displayName:</div>
</div>
@if (element is UIToggle)
{
var toggleElement = (UIToggle)element;
<div class="column value">
<div class="buttons">
<div class="button @GetToggleColor(true, toggleElement.getter())" onclick="@(() => toggleElement.setter(false))">Off</div>
<div class="button @GetToggleColor(false, toggleElement.getter())" onclick="@(() => toggleElement.setter(true))">On</div>
</div>
</div>
}
@if (element is UISlider)
{
var sliderElement = (UISlider)element;
<div class="column value">
<div class="slider">
<SliderControl Value="@(sliderElement.getter())" OnValueChanged="@(sliderElement.setter)" Min="@(sliderElement.min)" Max="@(sliderElement.max)" Step="@(sliderElement.step)"></SliderControl>
</div>
</div>
}
@if (element is UICyclerBase)
{
var cyclerElement = (UICyclerBase)element;
<div class="column value">
<div class="cycling-selector">
<div class="cycling-controls">
<button class="arrow left" onclick="@(() => cyclerElement.CycleLeft())">‹</button>
<span class="value" id="current-value">@cyclerElement.onGet()</span>
<button class="arrow right" onclick="@(() => cyclerElement.CycleRight())">›</button>
</div>
</div>
</div>
}
</div>
}
}
</div>
</div>
}
<div class="menu buttons">
<div class="button red" onclick="@(() => ButtonClose())">
Close
</div>
</div>
</div>
</root>
@code
{
public static string GetToggleColor(bool isOff, bool value)
{
if (isOff)
{
return value ? "gray" : "red";
}
return value ? "green" : "gray";
}
public List<UITab> tabs { get; set; } = new List<UITab>();
public UITab activeTab { get; set; }
protected override void OnAwake()
{
base.OnAwake();
var sortedValues = EasySaveNonGenericBase.typeToInst.Values.OrderBy(v => (v.uiTab != null) ? v.uiTab.order : 99999).ToList();
foreach (var sortedValue in sortedValues)
{
if (sortedValue == null)
continue;
tabs.Add(sortedValue.uiTab);
}
if (tabs != null && tabs.Count() > 0)
{
activeTab = tabs[0];
}
}
protected override void OnUpdate()
{
base.OnUpdate();
if (Input.EscapePressed)
{
Input.EscapePressed = false;
ButtonClose();
}
}
[ConCmd("open_options")]
public static void OpenOptions()
{
var inst = Game.ActiveScene.Components.Get<OptionsScreen>(true);
if (inst != null)
{
inst.Enabled = true;
}
else
{
var optionsScreenGO = Game.ActiveScene.CreateObject();
var screenPanel = optionsScreenGO.Components.Create<ScreenPanel>();
var optionsScreen = optionsScreenGO.Components.Create<OptionsScreen>();
}
}
void ButtonClose()
{
var soundHandle = Sound.Play("ui.navigate.back");
soundHandle.TargetMixer = Mixer.FindMixerByName("UI");
Close();
}
public void Close()
{
this.Enabled = false;
}
public bool wasMouseVisible { get; set; } = false;
protected override void OnEnabled()
{
wasMouseVisible = Mouse.Visible;
Mouse.Visible = true;
}
protected override void OnDisabled()
{
EasySaveNonGenericBase.SaveAll();
Mouse.Visible = wasMouseVisible;
}
/// <summary>
/// update every second
/// </summary>
protected override int BuildHash() => System.HashCode.Combine(RealTime.Now.CeilToInt());
}
@using Sandbox;
@inherits PanelComponent
@if (InSequence)
{
<root>
<div class="Main">
<div style="opacity: @Opacity">
<img src="@CurrentEntry.TexturePath" style="transform: scale(@Scale);">
</div>
</div>
</root>
}
@using Sandbox.UI;
@namespace Duccsoft
@inherits Panel
<root class=@OverlayClass>
<image id="spinner" src="ui/img/BufferingCircle.png"/>
<div id="bottomBar">
<div class="button symbol" onclick=@TogglePause>@PlayButtonIcon</div>
<div id="timecodeArea" class=@TimecodeAreaClass>
<div class="progress time">@ProgressText</div>
<div class="progress center">/</div>
<div class="progress time right">@DurationText</div>
</div>
<SliderControl id="progressBar" Min=@(0) Max=@DurationSeconds
Value:bind=@PlaybackTime OnValueChanged=@ProgressBarChanged ShowValueTooltip=@false/>
<div class="button symbol" onclick=@ToggleMute>@VolumeButtonIcon</div>
</div>
</root>
@using Sandbox
@using Sandbox.UI
@using LobbySystem
@inherits PanelComponent
@namespace LobbySystem.Examples
<root>
@if ( Dir is null )
{
<text></text>
}
else if ( Dir.MenuOpen || Dir.SuggestMenuOpen )
{
<div class="menu">
<div class="title">MULTIPLAYER LOBBY</div>
<div class="sub">@(Dir.MenuOpen ? "Choose a game mode" : "Suggest a mode to the host")</div>
<div class="row">
@for ( int i = 0; i < Dir.Modes.Count; i++ )
{
var idx = i;
<button class="mode" onclick=@(() => Dir.PickMode( idx ))>
<div class="k">@(idx + 1)</div>
<div class="n">@Dir.Modes[idx].DisplayName</div>
</button>
}
</div>
<button class="back" onclick=@(() => Dir.RequestCloseMenu())>Back to lobby [E]</button>
</div>
}
else
{
<div class="top">
@if ( Dir.RoundLive )
{
<div class="mode">@(Dir.ActiveMode?.DisplayName ?? "")</div>
<div class="timer @(Dir.TimeLeftSeconds < 15 ? "urgent" : "")">@TimerText</div>
}
else
{
<div class="lobby">LOBBY</div>
}
</div>
@if ( !Dir.RoundLive && !string.IsNullOrEmpty( Dir.StatusMessage ) )
{
<div class="status">@Dir.StatusMessage</div>
}
@if ( Dir.ChatVisible )
{
<div class="chat">@Dir.ChatLine</div>
}
<div class="hints">
<span>[WASD] Move</span>
<span>[Space] Jump</span>
@if ( !Dir.RoundLive )
{
<span>Walk to the pad and press [E] to start a round</span>
}
</div>
}
</root>
@code
{
LobbyDirector Dir => LobbyDirector.Current;
string TimerText
{
get
{
int t = Dir?.TimeLeftSeconds ?? 0;
if ( t < 0 ) t = 0;
return $"{t / 60:D2}:{t % 60:D2}";
}
}
protected override int BuildHash() => System.HashCode.Combine(
Dir?.State, Dir?.MenuOpen, Dir?.SuggestMenuOpen, Dir?.RoundLive,
Dir?.TimeLeftSeconds, Dir?.StatusMessage, Dir?.ChatLine, Dir?.ChatVisible );
}
@using System
@using System.Collections.Generic
@using System.Linq
@using Sandbox
@using Sandbox.UI
@namespace Skafinity
@inherits PanelComponent
@*
The optional drop-in settings panel for the Skafinity music engine.
Add this PanelComponent to a GameObject under a ScreenPanel (or WorldPanel). It finds a
SkafinityPlayer in the scene (or set Player explicitly) and offers the whole knob surface
as UI — you don't have to wire anything. A floating ♪ button toggles the board open/closed.
The engine needs nothing from this: SkafinityPlayer plays on its own. This panel is pure
convenience for players who want to tweak the vibe rather than tune it in the inspector.
The vibe editor is driven entirely from the library's VibeCodec field metadata for the
current genre: each field reports its voice (matrix row, or null for a GLOBAL knob) and
column (0 volume / 1 tone / 2 character / 3 extra). A new genre — or a new knob — is a pure
engine change; there is no field table here. s&box has no slider widget, so each knob is a
strip of tick cells (one per base-36 level the seed encodes); each change re-encodes the
vibe and restarts the player on a short debounce.
Re-theming: the palette lives as SCSS variables at the top of SkafinityMusicPanel.razor.scss.
Override those (or supply your own panel against the same SkafinityPlayer API) to restyle.
*@
<root class="@( IsOpen ? "open" : "" )">
@if ( !IsOpen )
{
<div class="fab" onclick="@Toggle">♪</div>
}
else
{
@{ var cfg = Player?.EffectiveConfig(); int genre = cfg?.Genre ?? 0; }
<div class="music">
<div class="header">
<div class="title">MUSIC</div>
<div class="close" onclick="@Toggle">✕</div>
</div>
<div class="divider"></div>
<div class="top">
<div class="row">
<div class="label">NOW PLAYING</div>
<div class="play-row">
<div class="seed-box">@Seed()</div>
<div class="btn" onclick="@CopySeed">@_copyLabel</div>
</div>
</div>
<div class="row">
<div class="label">SONG</div>
<div class="play-row">
<div class="btn wide" onclick="@( () => Step( -1 ) )">◀ Prev</div>
<div class="num">@( Player?.N ?? 0 )</div>
<div class="btn wide" onclick="@( () => Step( 1 ) )">Next ▶</div>
</div>
</div>
<div class="row">
<div class="label">PLAY A SEED</div>
<div class="play-row">
<TextEntry @ref="_tagEntry" placeholder="Paste vibe:tag:n (or a tag — blank = default)" class="tag-input" />
<div class="btn" onclick="@Play">Play</div>
<div class="btn" onclick="@UseDefault">Use default</div>
</div>
</div>
<div class="row">
<div class="label">MUTE</div>
<div class="play-row">
<div class="btn toggle @( Player?.Enabled == false ? "on" : "" )" onclick="@ToggleMute">@( Player?.Enabled == false ? "MUTED" : "PLAYING" )</div>
</div>
</div>
<div class="row">
<div class="label">VOLUME</div>
<div class="cells">
@foreach ( var v in VolumeSteps )
{
var vv = v;
<div class="cell @( ( Player?.Volume ?? 1f ) >= vv - 0.001f ? "filled" : "" )"
onclick="@( () => SetVolume( vv ) )">@VolLabel( vv )</div>
}
</div>
</div>
</div>
<div class="divider"></div>
<div class="row genre-row">
<div class="label">GENRE</div>
<div class="cells">
@for ( int g = 0; g < VibeCodec.GenreCount; g++ )
{
var gg = g;
<div class="cell choice @( g == genre ? "selected" : "" )"
onclick="@( () => SetGenre( gg ) )">@VibeCodec.Genres[g]</div>
}
<div class="btn reroll" onclick="@Reroll">🎲 Reroll</div>
<div class="btn reroll toggle @( Player?.RandomEverySong == true ? "on" : "" )"
onclick="@ToggleRandomEverySong">🎲 Random every song: @( Player?.RandomEverySong == true ? "ON" : "OFF" )</div>
<div class="btn" onclick="@Save">Save .wav</div>
</div>
</div>
<div class="label">VIBE — per-instrument mixer (tweak, then share the seed)</div>
<div class="matrix">
<div class="mrow mhead">
<div class="mvoice"></div>
@foreach ( var h in ColHeaders )
{
<div class="mcell mlabel">@h</div>
}
</div>
@foreach ( var (voice, cells) in InstrumentRows( genre ) )
{
<div class="mrow">
<div class="mvoice">@voice</div>
@for ( int col = 0; col < ColHeaders.Length; col++ )
{
var f = cells[col];
<div class="mcell">
@if ( f != null )
{
@Knob( f, cfg, f.Name == ColHeaders[col] ? "" : f.Name )
}
</div>
}
</div>
}
</div>
<div class="divider"></div>
<div class="label">GLOBAL</div>
<div class="vibe-grid">
@foreach ( var f in GlobalRows( genre ) )
{
<div class="vibe">@Knob( f, cfg, f.Name )</div>
}
</div>
@if ( _msg != null )
{
<div class="hint ok">@_msg</div>
}
</div>
}
</root>
@code
{
/// <summary>The player this panel drives. Leave unset to auto-find a <see cref="SkafinityPlayer"/>
/// in the scene on start.</summary>
[Property] public SkafinityPlayer Player { get; set; }
// One tick cell per base-36 level the seed encodes (VibeCodec.Levels).
static int UiTicks => VibeCodec.Levels;
static readonly string[] ColHeaders = { "VOLUME", "TONE", "CHARACTER", "EXTRA" };
// Volume control steps over the player's 0..2 range.
static readonly float[] VolumeSteps = { 0f, 0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f };
/// <summary>Whether the settings board is showing (toggled by the ♪ button).</summary>
public bool IsOpen { get; private set; }
TextEntry _tagEntry;
bool _tagInit;
string _copyLabel = "Copy";
string _msg;
protected override void OnStart()
{
Player ??= Scene.GetAllComponents<SkafinityPlayer>().FirstOrDefault();
if ( Player == null )
Log.Warning( "SkafinityMusicPanel: no SkafinityPlayer found in the scene — add one (or set Player)." );
}
protected override void OnUpdate()
{
// One-time seed of the text entry with the current tag once the board opens.
if ( !IsOpen ) { _tagInit = false; return; }
if ( !_tagInit && _tagEntry != null ) { _tagEntry.Text = Player?.Tag ?? ""; _tagInit = true; }
}
string Seed() => Player?.CurrentSeed ?? "—";
/// <summary>Open/close the settings board.</summary>
public void Toggle()
{
IsOpen = !IsOpen;
if ( !IsOpen ) _tagInit = false;
}
static string VolLabel( float v ) => v <= 0f ? "0" : $"{(int)Math.Round( v * 100 )}%";
// Instrument rows for the genre: each voice with its [vol, tone, character, extra] cells
// (null where a genre leaves a column empty). Order = the library's display/wire order.
static IEnumerable<(string Voice, VibeCodec.Field[] Cells)> InstrumentRows( int genre )
{
var order = new List<string>();
var byVoice = new Dictionary<string, VibeCodec.Field[]>();
foreach ( var f in VibeCodec.Fields( genre ) )
{
if ( f.Voice == null ) continue;
if ( !byVoice.TryGetValue( f.Voice, out var cells ) )
{
cells = new VibeCodec.Field[ColHeaders.Length];
byVoice[f.Voice] = cells;
order.Add( f.Voice );
}
if ( f.Column >= 0 && f.Column < cells.Length ) cells[f.Column] = f;
}
foreach ( var v in order ) yield return (v, byVoice[v]);
}
static IEnumerable<VibeCodec.Field> GlobalRows( int genre ) =>
VibeCodec.Fields( genre ).Where( f => f.Voice == null );
// Index of a field within the current genre's field list (what Player.SetVibe expects).
static int FieldIndex( int genre, VibeCodec.Field field )
{
var fields = VibeCodec.Fields( genre );
for ( int i = 0; i < fields.Count; i++ )
if ( ReferenceEquals( fields[i], field ) ) return i;
return -1;
}
// One knob: a name/value header over a strip of tick cells (or labeled choice cells).
RenderFragment Knob( VibeCodec.Field f, MusicGen.Config cfg, string label )
{
return @<text>
<div class="vibe-head">
<div class="vibe-name">@label</div>
<div class="vibe-val">@( cfg != null ? f.Display( cfg ) : "" )</div>
</div>
<div class="cells">
@{
int genre = cfg?.Genre ?? 0;
int idx = FieldIndex( genre, f );
}
@if ( f.Choices != null )
{
int sel = cfg != null ? (int)MathF.Round( f.GetNorm( cfg ) * (f.Choices.Length - 1) ) : 0;
@for ( int k = 0; k < f.Choices.Length; k++ )
{
var kk = k; var n = f.Choices.Length;
<div class="cell choice @( k == sel ? "selected" : "" )"
onclick="@( () => SetVibe( idx, kk / (float)(n - 1) ) )">@f.Choices[k]</div>
}
}
else
{
int sel = cfg != null ? (int)MathF.Round( f.GetNorm( cfg ) * (UiTicks - 1) ) : 0;
@for ( int k = 0; k < UiTicks; k++ )
{
var kk = k;
<div class="cell tick @( k <= sel ? "filled" : "" ) @( k == sel ? "selected" : "" )"
onclick="@( () => SetVibe( idx, kk / (float)(UiTicks - 1) ) )"></div>
}
}
</div>
</text>;
}
void Step( int d ) { Player?.StepN( d ); _msg = null; }
void ToggleMute() { if ( Player != null ) Player.Enabled = !Player.Enabled; }
void SetVolume( float v ) { if ( Player != null ) Player.Volume = v; }
void SetVibe( int index, float norm )
{
if ( index < 0 ) return;
Player?.SetVibe( index, norm );
_msg = null;
}
void SetGenre( int genre ) { Player?.SetGenre( genre ); _msg = null; }
void Play()
{
Player?.PlaySeed( _tagEntry?.Text );
_msg = Player != null ? $"Playing {Player.CurrentSeed}" : null;
}
void UseDefault()
{
Player?.SetTag( "" );
if ( _tagEntry != null ) _tagEntry.Text = "";
_msg = "Back to the default tag and vibe";
}
void CopySeed()
{
try { Clipboard.SetText( Seed() ); _copyLabel = "Copied!"; }
catch { _copyLabel = "—"; }
}
void Save()
{
var name = Player?.SaveCurrentToFile();
_msg = string.IsNullOrEmpty( name ) ? "Couldn't save song" : $"Saved {name} to your s&box data folder";
}
void Reroll() { Player?.RerollVibe(); _msg = "Rerolled every knob but the volumes"; }
void ToggleRandomEverySong()
{
if ( Player == null ) return;
bool on = !Player.RandomEverySong;
Player.RandomEverySong = on;
if ( on ) Player.RerollVibe();
_msg = on ? "Shuffle: every new song re-rolls the vibe (keeps your volumes)" : "Shuffle off";
}
protected override int BuildHash() =>
HashCode.Combine(
IsOpen, Player?.CurrentSeed, Player?.CurrentVibe, Player?.Enabled ?? true,
Player?.Volume ?? 1f, Player?.RandomEverySong ?? false, _msg, _copyLabel );
}
@using Sandbox
@using Sandbox.UI
@namespace PanelRenderTarget
@inherits ScreenPanel
<div class="screen">
<div class="title">Example</div>
<div class="debug" @onclick=@Click @onmouseover=@OnDebugMouseOver @onmouseout=@OnDebugMouseOut>
<div class="test-hover">Hover: @IsHovering</div>
<div>Pressed: @IsPressed</div>
<div>Clicks: @ClickCount</div>
<div>Mouse: @MousePosition</div>
<TextEntry @onclick=@TestClick Value:Bind=@test placeholder="Quantitée" />
</div>
<div class="cursor" style="left:@CursorLeft; top:@CursorTop;"></div>
</div>
@code
{
public Vector2 MousePosition { get; private set; }
public bool IsHovering { get; private set; }
public bool IsPressed { get; private set; }
public int ClickCount { get; private set; }
private string CursorLeft => $"{MousePosition.x}px";
private string CursorTop => $"{MousePosition.y}px";
public string test { get; set; } = "teste";
protected override void OnMouseMove(MousePanelEvent e)
{
base.OnMouseMove(e);
var root = FindRootPanel() as TargetRootPanel;
//MousePosition = root.MousePosition;
}
public void OnDebugMouseOver(PanelEvent e)
{
IsHovering = true;
}
public void OnDebugMouseOut(PanelEvent e)
{
IsHovering = false;
}
public void TestClick(PanelEvent e)
{
Log.Info("Clicked on text entry");
}
protected override void OnMouseDown(MousePanelEvent e)
{
base.OnMouseDown(e);
IsPressed = true;
}
protected override void OnMouseUp(MousePanelEvent e)
{
base.OnMouseUp(e);
IsPressed = false;
}
public void Click(PanelEvent e)
{
ClickCount++;
}
protected override int BuildHash()
{
StateHasChanged();
var hash = 17;
hash = hash * 31 + MousePosition.x.GetHashCode();
hash = hash * 31 + MousePosition.y.GetHashCode();
hash = hash * 31 + IsHovering.GetHashCode();
hash = hash * 31 + IsPressed.GetHashCode();
hash = hash * 31 + ClickCount.GetHashCode();
return hash;
}
}
<style>
.screen {
cursor: crosshair;
pointer-events: auto;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #06111f;
color: white;
font-size: 32px;
font-weight: bold;
}
.title {
position: absolute;
left: 40px;
top: 40px;
color: white;
font-size: 64px;
}
.debug {
position: absolute;
left: 40px;
top: 140px;
font-size: 28px;
color: #9fd4ff;
flex-direction: column;
width:400px;
}
.debug:hover {
background-color: aqua;
}
.debug:active {
background-color: red;
}
.cursor {
position: absolute;
width: 24px;
height: 24px;
background-color: red;
border-radius: 50%;
transform: translate(-50% -50%);
}
TextEntry {
background-color:green;
}
TextEntry:focus {
border-color: red;
}
</style>
@using Sandbox
@using Sandbox.UI
@namespace PanelRenderTarget
@inherits ScreenPanel
<div class="screen">
<div class="title">Example</div>
<div class="debug" @onclick=@Click @onmouseover=@OnDebugMouseOver @onmouseout=@OnDebugMouseOut>
<div class="test-hover">Hover: @IsHovering</div>
<div>Pressed: @IsPressed</div>
<div>Clicks: @ClickCount</div>
<div>Mouse: @MousePosition</div>
<TextEntry @onclick=@TestClick Value:Bind=@test placeholder="Quantitée" />
</div>
<div class="cursor" style="left:@CursorLeft; top:@CursorTop;"></div>
</div>
@code
{
public Vector2 MousePosition { get; private set; }
public bool IsHovering { get; private set; }
public bool IsPressed { get; private set; }
public int ClickCount { get; private set; }
private string CursorLeft => $"{MousePosition.x}px";
private string CursorTop => $"{MousePosition.y}px";
public string test { get; set; } = "teste";
protected override void OnMouseMove(MousePanelEvent e)
{
base.OnMouseMove(e);
var root = FindRootPanel() as TargetRootPanel;
//MousePosition = root.MousePosition;
}
public void OnDebugMouseOver(PanelEvent e)
{
IsHovering = true;
}
public void OnDebugMouseOut(PanelEvent e)
{
IsHovering = false;
}
public void TestClick(PanelEvent e)
{
Log.Info("Clicked on text entry");
}
protected override void OnMouseDown(MousePanelEvent e)
{
base.OnMouseDown(e);
IsPressed = true;
}
protected override void OnMouseUp(MousePanelEvent e)
{
base.OnMouseUp(e);
IsPressed = false;
}
public void Click(PanelEvent e)
{
ClickCount++;
}
protected override int BuildHash()
{
StateHasChanged();
var hash = 17;
hash = hash * 31 + MousePosition.x.GetHashCode();
hash = hash * 31 + MousePosition.y.GetHashCode();
hash = hash * 31 + IsHovering.GetHashCode();
hash = hash * 31 + IsPressed.GetHashCode();
hash = hash * 31 + ClickCount.GetHashCode();
return hash;
}
}
<style>
.screen {
cursor: crosshair;
pointer-events: auto;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #06111f;
color: white;
font-size: 32px;
font-weight: bold;
}
.title {
position: absolute;
left: 40px;
top: 40px;
color: white;
font-size: 64px;
}
.debug {
position: absolute;
left: 40px;
top: 140px;
font-size: 28px;
color: #9fd4ff;
flex-direction: column;
width:400px;
}
.debug:hover {
background-color: aqua;
}
.debug:active {
background-color: red;
}
.cursor {
position: absolute;
width: 24px;
height: 24px;
background-color: red;
border-radius: 50%;
transform: translate(-50% -50%);
}
TextEntry {
background-color:green;
}
TextEntry:focus {
border-color: red;
}
</style>
@using System.Collections.Generic;
@using System.Linq;
@using Sandbox;
@using Sandbox.UI;
@namespace TwitchAPI.Examples
<root>
<div class="username">
<div class="badges">
@foreach (var badge in ChatMessage.User.Badges)
{
<div class="badge @badge" />
}
</div>
@if (HasAvatarImages)
{
<img class="avatar" [email protected]() />
}
<label class="name" style="color: @ChatMessage.User.Color.Hex">@ChatMessage.Username</label>
<label>:</label>
</div>
<div class="message">
@foreach (var fragment in Fragments)
{
if (fragment.Emote is not null)
{
<img class="emote" src="@fragment.Emote" />
}
else
{
<label>@fragment.Text</label>
}
}
</div>
</root>
@code
{
public TwitchChatMessage ChatMessage { get; set; }
public float Lifetime { get; set; } = 10f;
public float FadeTime { get; set; } = 0.5f;
public bool HasAvatarImages { get; set; } = false;
List<MessageFragment> Fragments { get; set; } = new();
TimeSince TimeSinceCreated = 0f;
record MessageFragment(string Text, string Emote);
protected override void OnParametersSet()
{
base.OnParametersSet();
Fragments.Clear();
var text = ChatMessage.Message;
var emotes = ChatMessage.Emotes;
var currentEmote = emotes.FirstOrDefault();
var currentIndex = 0;
while (currentEmote is not null)
{
var textPrior = text.Substring(currentIndex, currentEmote.StartingCharacter - currentIndex);
if (!string.IsNullOrWhiteSpace(textPrior))
{
foreach (var word in textPrior.Split(' '))
{
if (string.IsNullOrWhiteSpace(word)) continue;
Fragments.Add(new MessageFragment(word, null));
}
}
var emoteText = text.Substring(currentEmote.StartingCharacter, currentEmote.EndingCharacter - currentEmote.StartingCharacter + 1);
Fragments.Add(new MessageFragment(emoteText, currentEmote.GetImageUrl()));
currentIndex = currentEmote.EndingCharacter + 1;
currentEmote = emotes.FirstOrDefault(e => e.StartingCharacter >= currentIndex);
}
var textAfter = text.Substring(currentIndex);
if (!string.IsNullOrWhiteSpace(textAfter))
{
foreach (var word in textAfter.Split(' '))
{
if (string.IsNullOrWhiteSpace(word)) continue;
Fragments.Add(new MessageFragment(word, null));
}
}
}
public override void Tick()
{
if (TimeSinceCreated >= Lifetime)
{
var opacity = 1f - ((TimeSinceCreated - Lifetime) / FadeTime);
if (opacity <= 0f)
{
Delete();
}
else
{
Style.Opacity = opacity;
}
}
}
}@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
);
}
}
@using System.Collections.Generic;
@using System.Linq;
@using Sandbox;
@using Sandbox.UI;
@namespace TwitchAPI.Examples
<root>
<div class="username">
<div class="badges">
@foreach (var badge in ChatMessage.User.Badges)
{
<div class="badge @badge" />
}
</div>
@if (HasAvatarImages)
{
<img class="avatar" [email protected]() />
}
<label class="name" style="color: @ChatMessage.User.Color.Hex">@ChatMessage.Username</label>
<label>:</label>
</div>
<div class="message">
@foreach (var fragment in Fragments)
{
if (fragment.Emote is not null)
{
<img class="emote" src="@fragment.Emote" />
}
else
{
<label>@fragment.Text</label>
}
}
</div>
</root>
@code
{
public TwitchChatMessage ChatMessage { get; set; }
public float Lifetime { get; set; } = 10f;
public float FadeTime { get; set; } = 0.5f;
public bool HasAvatarImages { get; set; } = false;
List<MessageFragment> Fragments { get; set; } = new();
TimeSince TimeSinceCreated = 0f;
record MessageFragment(string Text, string Emote);
protected override void OnParametersSet()
{
base.OnParametersSet();
Fragments.Clear();
var text = ChatMessage.Message;
var emotes = ChatMessage.Emotes;
var currentEmote = emotes.FirstOrDefault();
var currentIndex = 0;
while (currentEmote is not null)
{
var textPrior = text.Substring(currentIndex, currentEmote.StartingCharacter - currentIndex);
if (!string.IsNullOrWhiteSpace(textPrior))
{
foreach (var word in textPrior.Split(' '))
{
if (string.IsNullOrWhiteSpace(word)) continue;
Fragments.Add(new MessageFragment(word, null));
}
}
var emoteText = text.Substring(currentEmote.StartingCharacter, currentEmote.EndingCharacter - currentEmote.StartingCharacter + 1);
Fragments.Add(new MessageFragment(emoteText, currentEmote.GetImageUrl()));
currentIndex = currentEmote.EndingCharacter + 1;
currentEmote = emotes.FirstOrDefault(e => e.StartingCharacter >= currentIndex);
}
var textAfter = text.Substring(currentIndex);
if (!string.IsNullOrWhiteSpace(textAfter))
{
foreach (var word in textAfter.Split(' '))
{
if (string.IsNullOrWhiteSpace(word)) continue;
Fragments.Add(new MessageFragment(word, null));
}
}
}
public override void Tick()
{
if (TimeSinceCreated >= Lifetime)
{
var opacity = 1f - ((TimeSinceCreated - Lifetime) / FadeTime);
if (opacity <= 0f)
{
Delete();
}
else
{
Style.Opacity = opacity;
}
}
}
}@using Sandbox;
@inherits PanelComponent
@if (InSequence)
{
<root>
<div class="Main">
<div style="opacity: @Opacity">
<img src="@CurrentEntry.TexturePath" style="transform: scale(@Scale);">
</div>
</div>
</root>
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox
<root>
<div class="title">@MyStringValue</div>
</root>
@code
{
[Property, TextArea] public string MyStringValue { get; set; } = "Hello World!";
protected override int BuildHash() => System.HashCode.Combine( MyStringValue );
}
@using System
@using System.Collections.Generic
@using System.Linq
@using Sandbox
@using Sandbox.UI
@namespace Skafinity
@inherits PanelComponent
@*
The optional drop-in settings panel for the Skafinity music engine.
Add this PanelComponent to a GameObject under a ScreenPanel (or WorldPanel). It finds a
SkafinityPlayer in the scene (or set Player explicitly) and offers the whole knob surface
as UI — you don't have to wire anything. A floating ♪ button toggles the board open/closed.
The engine needs nothing from this: SkafinityPlayer plays on its own. This panel is pure
convenience for players who want to tweak the vibe rather than tune it in the inspector.
The vibe editor is driven entirely from the library's VibeCodec field metadata for the
current genre: each field reports its voice (matrix row, or null for a GLOBAL knob) and
column (0 volume / 1 tone / 2 character / 3 extra). A new genre — or a new knob — is a pure
engine change; there is no field table here. s&box has no slider widget, so each knob is a
strip of tick cells (one per base-36 level the seed encodes); each change re-encodes the
vibe and restarts the player on a short debounce.
Re-theming: the palette lives as SCSS variables at the top of SkafinityMusicPanel.razor.scss.
Override those (or supply your own panel against the same SkafinityPlayer API) to restyle.
*@
<root class="@( IsOpen ? "open" : "" )">
@if ( !IsOpen )
{
<div class="fab" onclick="@Toggle">♪</div>
}
else
{
@{ var cfg = Player?.EffectiveConfig(); int genre = cfg?.Genre ?? 0; }
<div class="music">
<div class="header">
<div class="title">MUSIC</div>
<div class="close" onclick="@Toggle">✕</div>
</div>
<div class="divider"></div>
<div class="top">
<div class="row">
<div class="label">NOW PLAYING</div>
<div class="play-row">
<div class="seed-box">@Seed()</div>
<div class="btn" onclick="@CopySeed">@_copyLabel</div>
</div>
</div>
<div class="row">
<div class="label">SONG</div>
<div class="play-row">
<div class="btn wide" onclick="@( () => Step( -1 ) )">◀ Prev</div>
<div class="num">@( Player?.N ?? 0 )</div>
<div class="btn wide" onclick="@( () => Step( 1 ) )">Next ▶</div>
</div>
</div>
<div class="row">
<div class="label">PLAY A SEED</div>
<div class="play-row">
<TextEntry @ref="_tagEntry" placeholder="Paste vibe:tag:n (or a tag — blank = default)" class="tag-input" />
<div class="btn" onclick="@Play">Play</div>
<div class="btn" onclick="@UseDefault">Use default</div>
</div>
</div>
<div class="row">
<div class="label">MUTE</div>
<div class="play-row">
<div class="btn toggle @( Player?.Enabled == false ? "on" : "" )" onclick="@ToggleMute">@( Player?.Enabled == false ? "MUTED" : "PLAYING" )</div>
</div>
</div>
<div class="row">
<div class="label">VOLUME</div>
<div class="cells">
@foreach ( var v in VolumeSteps )
{
var vv = v;
<div class="cell @( ( Player?.Volume ?? 1f ) >= vv - 0.001f ? "filled" : "" )"
onclick="@( () => SetVolume( vv ) )">@VolLabel( vv )</div>
}
</div>
</div>
</div>
<div class="divider"></div>
<div class="row genre-row">
<div class="label">GENRE</div>
<div class="cells">
@for ( int g = 0; g < VibeCodec.GenreCount; g++ )
{
var gg = g;
<div class="cell choice @( g == genre ? "selected" : "" )"
onclick="@( () => SetGenre( gg ) )">@VibeCodec.Genres[g]</div>
}
<div class="btn reroll" onclick="@Reroll">🎲 Reroll</div>
<div class="btn reroll toggle @( Player?.RandomEverySong == true ? "on" : "" )"
onclick="@ToggleRandomEverySong">🎲 Random every song: @( Player?.RandomEverySong == true ? "ON" : "OFF" )</div>
<div class="btn" onclick="@Save">Save .wav</div>
</div>
</div>
<div class="label">VIBE — per-instrument mixer (tweak, then share the seed)</div>
<div class="matrix">
<div class="mrow mhead">
<div class="mvoice"></div>
@foreach ( var h in ColHeaders )
{
<div class="mcell mlabel">@h</div>
}
</div>
@foreach ( var (voice, cells) in InstrumentRows( genre ) )
{
<div class="mrow">
<div class="mvoice">@voice</div>
@for ( int col = 0; col < ColHeaders.Length; col++ )
{
var f = cells[col];
<div class="mcell">
@if ( f != null )
{
@Knob( f, cfg, f.Name == ColHeaders[col] ? "" : f.Name )
}
</div>
}
</div>
}
</div>
<div class="divider"></div>
<div class="label">GLOBAL</div>
<div class="vibe-grid">
@foreach ( var f in GlobalRows( genre ) )
{
<div class="vibe">@Knob( f, cfg, f.Name )</div>
}
</div>
@if ( _msg != null )
{
<div class="hint ok">@_msg</div>
}
</div>
}
</root>
@code
{
/// <summary>The player this panel drives. Leave unset to auto-find a <see cref="SkafinityPlayer"/>
/// in the scene on start.</summary>
[Property] public SkafinityPlayer Player { get; set; }
// One tick cell per base-36 level the seed encodes (VibeCodec.Levels).
static int UiTicks => VibeCodec.Levels;
static readonly string[] ColHeaders = { "VOLUME", "TONE", "CHARACTER", "EXTRA" };
// Volume control steps over the player's 0..2 range.
static readonly float[] VolumeSteps = { 0f, 0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f };
/// <summary>Whether the settings board is showing (toggled by the ♪ button).</summary>
public bool IsOpen { get; private set; }
TextEntry _tagEntry;
bool _tagInit;
string _copyLabel = "Copy";
string _msg;
protected override void OnStart()
{
Player ??= Scene.GetAllComponents<SkafinityPlayer>().FirstOrDefault();
if ( Player == null )
Log.Warning( "SkafinityMusicPanel: no SkafinityPlayer found in the scene — add one (or set Player)." );
}
protected override void OnUpdate()
{
// One-time seed of the text entry with the current tag once the board opens.
if ( !IsOpen ) { _tagInit = false; return; }
if ( !_tagInit && _tagEntry != null ) { _tagEntry.Text = Player?.Tag ?? ""; _tagInit = true; }
}
string Seed() => Player?.CurrentSeed ?? "—";
/// <summary>Open/close the settings board.</summary>
public void Toggle()
{
IsOpen = !IsOpen;
if ( !IsOpen ) _tagInit = false;
}
static string VolLabel( float v ) => v <= 0f ? "0" : $"{(int)Math.Round( v * 100 )}%";
// Instrument rows for the genre: each voice with its [vol, tone, character, extra] cells
// (null where a genre leaves a column empty). Order = the library's display/wire order.
static IEnumerable<(string Voice, VibeCodec.Field[] Cells)> InstrumentRows( int genre )
{
var order = new List<string>();
var byVoice = new Dictionary<string, VibeCodec.Field[]>();
foreach ( var f in VibeCodec.Fields( genre ) )
{
if ( f.Voice == null ) continue;
if ( !byVoice.TryGetValue( f.Voice, out var cells ) )
{
cells = new VibeCodec.Field[ColHeaders.Length];
byVoice[f.Voice] = cells;
order.Add( f.Voice );
}
if ( f.Column >= 0 && f.Column < cells.Length ) cells[f.Column] = f;
}
foreach ( var v in order ) yield return (v, byVoice[v]);
}
static IEnumerable<VibeCodec.Field> GlobalRows( int genre ) =>
VibeCodec.Fields( genre ).Where( f => f.Voice == null );
// Index of a field within the current genre's field list (what Player.SetVibe expects).
static int FieldIndex( int genre, VibeCodec.Field field )
{
var fields = VibeCodec.Fields( genre );
for ( int i = 0; i < fields.Count; i++ )
if ( ReferenceEquals( fields[i], field ) ) return i;
return -1;
}
// One knob: a name/value header over a strip of tick cells (or labeled choice cells).
RenderFragment Knob( VibeCodec.Field f, MusicGen.Config cfg, string label )
{
return @<text>
<div class="vibe-head">
<div class="vibe-name">@label</div>
<div class="vibe-val">@( cfg != null ? f.Display( cfg ) : "" )</div>
</div>
<div class="cells">
@{
int genre = cfg?.Genre ?? 0;
int idx = FieldIndex( genre, f );
}
@if ( f.Choices != null )
{
int sel = cfg != null ? (int)MathF.Round( f.GetNorm( cfg ) * (f.Choices.Length - 1) ) : 0;
@for ( int k = 0; k < f.Choices.Length; k++ )
{
var kk = k; var n = f.Choices.Length;
<div class="cell choice @( k == sel ? "selected" : "" )"
onclick="@( () => SetVibe( idx, kk / (float)(n - 1) ) )">@f.Choices[k]</div>
}
}
else
{
int sel = cfg != null ? (int)MathF.Round( f.GetNorm( cfg ) * (UiTicks - 1) ) : 0;
@for ( int k = 0; k < UiTicks; k++ )
{
var kk = k;
<div class="cell tick @( k <= sel ? "filled" : "" ) @( k == sel ? "selected" : "" )"
onclick="@( () => SetVibe( idx, kk / (float)(UiTicks - 1) ) )"></div>
}
}
</div>
</text>;
}
void Step( int d ) { Player?.StepN( d ); _msg = null; }
void ToggleMute() { if ( Player != null ) Player.Enabled = !Player.Enabled; }
void SetVolume( float v ) { if ( Player != null ) Player.Volume = v; }
void SetVibe( int index, float norm )
{
if ( index < 0 ) return;
Player?.SetVibe( index, norm );
_msg = null;
}
void SetGenre( int genre ) { Player?.SetGenre( genre ); _msg = null; }
void Play()
{
Player?.PlaySeed( _tagEntry?.Text );
_msg = Player != null ? $"Playing {Player.CurrentSeed}" : null;
}
void UseDefault()
{
Player?.SetTag( "" );
if ( _tagEntry != null ) _tagEntry.Text = "";
_msg = "Back to the default tag and vibe";
}
void CopySeed()
{
try { Clipboard.SetText( Seed() ); _copyLabel = "Copied!"; }
catch { _copyLabel = "—"; }
}
void Save()
{
var name = Player?.SaveCurrentToFile();
_msg = string.IsNullOrEmpty( name ) ? "Couldn't save song" : $"Saved {name} to your s&box data folder";
}
void Reroll() { Player?.RerollVibe(); _msg = "Rerolled every knob but the volumes"; }
void ToggleRandomEverySong()
{
if ( Player == null ) return;
bool on = !Player.RandomEverySong;
Player.RandomEverySong = on;
if ( on ) Player.RerollVibe();
_msg = on ? "Shuffle: every new song re-rolls the vibe (keeps your volumes)" : "Shuffle off";
}
protected override int BuildHash() =>
HashCode.Combine(
IsOpen, Player?.CurrentSeed, Player?.CurrentVibe, Player?.Enabled ?? true,
Player?.Volume ?? 1f, Player?.RandomEverySong ?? false, _msg, _copyLabel );
}
@using System
@using System.Threading.Tasks
@using Sandbox
@using Sandbox.UI
@inherits WebPanel
<style>
/* .scss files don't work in libraries. https://github.com/Facepunch/sbox-issues/issues/4813 */
prompt {
width: 100%;
pointer-events: all;
}
</style>
@code {
public readonly TaskCompletionSource<bool> Purchased;
public Prompt( string URL )
{
Url = URL;
Purchased = new TaskCompletionSource<bool>();
if ( !Game.ActiveScene.Components.TryGet( out ScreenPanel panel ) )
{
panel = Game.ActiveScene.Components.Create<ScreenPanel>();
panel.GetPanel().ElementName = "monetization";
panel.ZIndex = int.MaxValue;
}
Parent = panel.GetPanel();
}
public override void OnDeleted()
{
if ( !Game.ActiveScene.Components.TryGet( out ScreenPanel panel ) )
{
return;
}
if ( panel.GetPanel().HasChildren )
{
return;
}
panel.Destroy();
}
protected override void OnAfterTreeRender( bool firstTime )
{
// This was the only way I could think of to get the
// Steam browser to send data back data to the game.
// If you have a better way let me know.
switch ( Surface.PageTitle )
{
case "finished": Delete();
break;
case "purchased": Purchased.TrySetResult( true );
break;
case "cancelled": Purchased.TrySetResult( false );
break;
}
}
protected override int BuildHash() => HashCode.Combine( Surface.PageTitle );
}
@using Sandbox.UI;
@namespace Duccsoft
@inherits Panel
<root class=@OverlayClass>
<image id="spinner" src="ui/img/BufferingCircle.png"/>
<div id="bottomBar">
<div class="button symbol" onclick=@TogglePause>@PlayButtonIcon</div>
<div id="timecodeArea" class=@TimecodeAreaClass>
<div class="progress time">@ProgressText</div>
<div class="progress center">/</div>
<div class="progress time right">@DurationText</div>
</div>
<SliderControl id="progressBar" Min=@(0) Max=@DurationSeconds
Value:bind=@PlaybackTime OnValueChanged=@ProgressBarChanged ShowValueTooltip=@false/>
<div class="button symbol" onclick=@ToggleMute>@VolumeButtonIcon</div>
</div>
</root>
@using Sandbox.UI;
@namespace Duccsoft
@inherits Panel
<root class="video-texture">
@if ( ShowControls )
{
<VideoControlOverlay VideoPanel=@this AutoHide=@AutoHideControls AutoHideDelay=@AutoHideDelay/>
}
</root>
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox
<root>
<div class="title">@MyStringValue</div>
</root>
@code
{
[Property, TextArea] public string MyStringValue { get; set; } = "Hello World!";
protected override int BuildHash() => System.HashCode.Combine( MyStringValue );
}
@using System
@using System.Collections.Generic
@using System.Linq
@using Sandbox
@using Sandbox.UI
@namespace Skafinity
@inherits PanelComponent
@*
The optional drop-in settings panel for the Skafinity music engine.
Add this PanelComponent to a GameObject under a ScreenPanel (or WorldPanel). It finds a
SkafinityPlayer in the scene (or set Player explicitly) and offers the whole knob surface
as UI — you don't have to wire anything. A floating ♪ button toggles the board open/closed.
The engine needs nothing from this: SkafinityPlayer plays on its own. This panel is pure
convenience for players who want to tweak the vibe rather than tune it in the inspector.
The vibe editor is driven entirely from the library's VibeCodec field metadata for the
current genre: each field reports its voice (matrix row, or null for a GLOBAL knob) and
column (0 volume / 1 tone / 2 character / 3 extra). A new genre — or a new knob — is a pure
engine change; there is no field table here. s&box has no slider widget, so each knob is a
strip of tick cells (one per base-36 level the seed encodes); each change re-encodes the
vibe and restarts the player on a short debounce.
Re-theming: the palette lives as SCSS variables at the top of SkafinityMusicPanel.razor.scss.
Override those (or supply your own panel against the same SkafinityPlayer API) to restyle.
*@
<root class="@( IsOpen ? "open" : "" )">
@if ( !IsOpen )
{
<div class="fab" onclick="@Toggle">♪</div>
}
else
{
@{ var cfg = Player?.EffectiveConfig(); int genre = cfg?.Genre ?? 0; }
<div class="music">
<div class="header">
<div class="title">MUSIC</div>
<div class="close" onclick="@Toggle">✕</div>
</div>
<div class="divider"></div>
<div class="top">
<div class="row">
<div class="label">NOW PLAYING</div>
<div class="play-row">
<div class="seed-box">@Seed()</div>
<div class="btn" onclick="@CopySeed">@_copyLabel</div>
</div>
</div>
<div class="row">
<div class="label">SONG</div>
<div class="play-row">
<div class="btn wide" onclick="@( () => Step( -1 ) )">◀ Prev</div>
<div class="num">@( Player?.N ?? 0 )</div>
<div class="btn wide" onclick="@( () => Step( 1 ) )">Next ▶</div>
</div>
</div>
<div class="row">
<div class="label">PLAY A SEED</div>
<div class="play-row">
<TextEntry @ref="_tagEntry" placeholder="Paste vibe:tag:n (or a tag — blank = default)" class="tag-input" />
<div class="btn" onclick="@Play">Play</div>
<div class="btn" onclick="@UseDefault">Use default</div>
</div>
</div>
<div class="row">
<div class="label">MUTE</div>
<div class="play-row">
<div class="btn toggle @( Player?.Enabled == false ? "on" : "" )" onclick="@ToggleMute">@( Player?.Enabled == false ? "MUTED" : "PLAYING" )</div>
</div>
</div>
<div class="row">
<div class="label">VOLUME</div>
<div class="cells">
@foreach ( var v in VolumeSteps )
{
var vv = v;
<div class="cell @( ( Player?.Volume ?? 1f ) >= vv - 0.001f ? "filled" : "" )"
onclick="@( () => SetVolume( vv ) )">@VolLabel( vv )</div>
}
</div>
</div>
</div>
<div class="divider"></div>
<div class="row genre-row">
<div class="label">GENRE</div>
<div class="cells">
@for ( int g = 0; g < VibeCodec.GenreCount; g++ )
{
var gg = g;
<div class="cell choice @( g == genre ? "selected" : "" )"
onclick="@( () => SetGenre( gg ) )">@VibeCodec.Genres[g]</div>
}
<div class="btn reroll" onclick="@Reroll">🎲 Reroll</div>
<div class="btn reroll toggle @( Player?.RandomEverySong == true ? "on" : "" )"
onclick="@ToggleRandomEverySong">🎲 Random every song: @( Player?.RandomEverySong == true ? "ON" : "OFF" )</div>
<div class="btn" onclick="@Save">Save .wav</div>
</div>
</div>
<div class="label">VIBE — per-instrument mixer (tweak, then share the seed)</div>
<div class="matrix">
<div class="mrow mhead">
<div class="mvoice"></div>
@foreach ( var h in ColHeaders )
{
<div class="mcell mlabel">@h</div>
}
</div>
@foreach ( var (voice, cells) in InstrumentRows( genre ) )
{
<div class="mrow">
<div class="mvoice">@voice</div>
@for ( int col = 0; col < ColHeaders.Length; col++ )
{
var f = cells[col];
<div class="mcell">
@if ( f != null )
{
@Knob( f, cfg, f.Name == ColHeaders[col] ? "" : f.Name )
}
</div>
}
</div>
}
</div>
<div class="divider"></div>
<div class="label">GLOBAL</div>
<div class="vibe-grid">
@foreach ( var f in GlobalRows( genre ) )
{
<div class="vibe">@Knob( f, cfg, f.Name )</div>
}
</div>
@if ( _msg != null )
{
<div class="hint ok">@_msg</div>
}
</div>
}
</root>
@code
{
/// <summary>The player this panel drives. Leave unset to auto-find a <see cref="SkafinityPlayer"/>
/// in the scene on start.</summary>
[Property] public SkafinityPlayer Player { get; set; }
// One tick cell per base-36 level the seed encodes (VibeCodec.Levels).
static int UiTicks => VibeCodec.Levels;
static readonly string[] ColHeaders = { "VOLUME", "TONE", "CHARACTER", "EXTRA" };
// Volume control steps over the player's 0..2 range.
static readonly float[] VolumeSteps = { 0f, 0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f };
/// <summary>Whether the settings board is showing (toggled by the ♪ button).</summary>
public bool IsOpen { get; private set; }
TextEntry _tagEntry;
bool _tagInit;
string _copyLabel = "Copy";
string _msg;
protected override void OnStart()
{
Player ??= Scene.GetAllComponents<SkafinityPlayer>().FirstOrDefault();
if ( Player == null )
Log.Warning( "SkafinityMusicPanel: no SkafinityPlayer found in the scene — add one (or set Player)." );
}
protected override void OnUpdate()
{
// One-time seed of the text entry with the current tag once the board opens.
if ( !IsOpen ) { _tagInit = false; return; }
if ( !_tagInit && _tagEntry != null ) { _tagEntry.Text = Player?.Tag ?? ""; _tagInit = true; }
}
string Seed() => Player?.CurrentSeed ?? "—";
/// <summary>Open/close the settings board.</summary>
public void Toggle()
{
IsOpen = !IsOpen;
if ( !IsOpen ) _tagInit = false;
}
static string VolLabel( float v ) => v <= 0f ? "0" : $"{(int)Math.Round( v * 100 )}%";
// Instrument rows for the genre: each voice with its [vol, tone, character, extra] cells
// (null where a genre leaves a column empty). Order = the library's display/wire order.
static IEnumerable<(string Voice, VibeCodec.Field[] Cells)> InstrumentRows( int genre )
{
var order = new List<string>();
var byVoice = new Dictionary<string, VibeCodec.Field[]>();
foreach ( var f in VibeCodec.Fields( genre ) )
{
if ( f.Voice == null ) continue;
if ( !byVoice.TryGetValue( f.Voice, out var cells ) )
{
cells = new VibeCodec.Field[ColHeaders.Length];
byVoice[f.Voice] = cells;
order.Add( f.Voice );
}
if ( f.Column >= 0 && f.Column < cells.Length ) cells[f.Column] = f;
}
foreach ( var v in order ) yield return (v, byVoice[v]);
}
static IEnumerable<VibeCodec.Field> GlobalRows( int genre ) =>
VibeCodec.Fields( genre ).Where( f => f.Voice == null );
// Index of a field within the current genre's field list (what Player.SetVibe expects).
static int FieldIndex( int genre, VibeCodec.Field field )
{
var fields = VibeCodec.Fields( genre );
for ( int i = 0; i < fields.Count; i++ )
if ( ReferenceEquals( fields[i], field ) ) return i;
return -1;
}
// One knob: a name/value header over a strip of tick cells (or labeled choice cells).
RenderFragment Knob( VibeCodec.Field f, MusicGen.Config cfg, string label )
{
return @<text>
<div class="vibe-head">
<div class="vibe-name">@label</div>
<div class="vibe-val">@( cfg != null ? f.Display( cfg ) : "" )</div>
</div>
<div class="cells">
@{
int genre = cfg?.Genre ?? 0;
int idx = FieldIndex( genre, f );
}
@if ( f.Choices != null )
{
int sel = cfg != null ? (int)MathF.Round( f.GetNorm( cfg ) * (f.Choices.Length - 1) ) : 0;
@for ( int k = 0; k < f.Choices.Length; k++ )
{
var kk = k; var n = f.Choices.Length;
<div class="cell choice @( k == sel ? "selected" : "" )"
onclick="@( () => SetVibe( idx, kk / (float)(n - 1) ) )">@f.Choices[k]</div>
}
}
else
{
int sel = cfg != null ? (int)MathF.Round( f.GetNorm( cfg ) * (UiTicks - 1) ) : 0;
@for ( int k = 0; k < UiTicks; k++ )
{
var kk = k;
<div class="cell tick @( k <= sel ? "filled" : "" ) @( k == sel ? "selected" : "" )"
onclick="@( () => SetVibe( idx, kk / (float)(UiTicks - 1) ) )"></div>
}
}
</div>
</text>;
}
void Step( int d ) { Player?.StepN( d ); _msg = null; }
void ToggleMute() { if ( Player != null ) Player.Enabled = !Player.Enabled; }
void SetVolume( float v ) { if ( Player != null ) Player.Volume = v; }
void SetVibe( int index, float norm )
{
if ( index < 0 ) return;
Player?.SetVibe( index, norm );
_msg = null;
}
void SetGenre( int genre ) { Player?.SetGenre( genre ); _msg = null; }
void Play()
{
Player?.PlaySeed( _tagEntry?.Text );
_msg = Player != null ? $"Playing {Player.CurrentSeed}" : null;
}
void UseDefault()
{
Player?.SetTag( "" );
if ( _tagEntry != null ) _tagEntry.Text = "";
_msg = "Back to the default tag and vibe";
}
void CopySeed()
{
try { Clipboard.SetText( Seed() ); _copyLabel = "Copied!"; }
catch { _copyLabel = "—"; }
}
void Save()
{
var name = Player?.SaveCurrentToFile();
_msg = string.IsNullOrEmpty( name ) ? "Couldn't save song" : $"Saved {name} to your s&box data folder";
}
void Reroll() { Player?.RerollVibe(); _msg = "Rerolled every knob but the volumes"; }
void ToggleRandomEverySong()
{
if ( Player == null ) return;
bool on = !Player.RandomEverySong;
Player.RandomEverySong = on;
if ( on ) Player.RerollVibe( includeVolumes: true, includeGenre: true );
_msg = on ? "Shuffle: every new song randomizes every knob" : "Shuffle off";
}
protected override int BuildHash() =>
HashCode.Combine(
IsOpen, Player?.CurrentSeed, Player?.CurrentVibe, Player?.Enabled ?? true,
Player?.Volume ?? 1f, Player?.RandomEverySong ?? false, _msg, _copyLabel );
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace TwitchAPI.Examples
<root>
</root>
@code
{
/// <summary>
/// The username of the Twitch Chat to connect to.
/// </summary>
[Property] string Username { get; set; }
/// <summary>
/// Whether or not to include the user's avatar images in the chat.
/// </summary>
[Property] bool HasAvatarImages { get; set; } = false;
/// <summary>
/// How long a message should be displayed for (in seconds).
/// </summary>
[Property, Group("Timing")] float MessageTime { get; set; } = 30f;
/// <summary>
/// How long a message should take to fade out (in seconds).
/// </summary>
[Property, Group("Timing")] float FadeTime { get; set; } = 0.5f;
/// <summary>
/// The position of the chat on the screen.
/// </summary>
[Property, Group("Styling")] Vector2 ChatPosition { get; set; } = new(5f, 95f);
/// <summary>
/// The size of the chat. Set to (0, 0) to scale automatically.
/// </summary>
[Property, Group("Styling")] Vector2 ChatSize { get; set; } = new(400f, 600f);
/// <summary>
/// The background color of the chat.
/// </summary>
[Property, Group("Styling")] Color BackgroundColor { get; set; } = Color.Transparent;
/// <summary>
/// The color of the text in the chat.
/// </summary>
[Property, Group("Styling")] Color TextColor { get; set; } = Color.White;
/// <summary>
/// The size of the text in the chat.
/// </summary>
[Property, Group("Styling")] float TextSize { get; set; } = 16f;
/// <summary>
/// The font family to use for the text in the chat.
/// </summary>
[Property, Group("Styling")] string FontFamily { get; set; } = "Poppins";
/// <summary>
/// The size of the gaps between messages in the chat.
/// </summary>
[Property, Group("Styling")] float GapSize { get; set; } = 8f;
/// <summary>
/// The padding around messages in the chat.
/// </summary>
[Property, Group("Styling")] float Padding { get; set; } = 8f;
/// <summary>
/// Whether or not to reverse the order of messages in the chat.
/// </summary>
[Property, Group("Styling")] bool ReverseOrder { get; set; } = false;
/// <summary>
/// Whether or not to use custom images for subscriber badges.
/// </summary>
[Property, Group("Badges")] bool HasCustomSubscriberBadges { get; set; } = false;
/// <summary>
/// The custom subscriber badges to use.
/// </summary>
[Property, Group("Badges"), InlineEditor, ShowIf(nameof(TwitchChatExample.HasCustomSubscriberBadges), true)] CustomSubscriberBadges SubscriberBadges { get; set; } = new();
TwitchChatConnection Twitch;
protected override void OnEnabled()
{
if (string.IsNullOrWhiteSpace(Username)) return;
Twitch = new TwitchChatConnection(Username);
Twitch.OnMessageReceived += OnMessageReceived;
Twitch.OnMessageRemoved += OnMessageRemoved;
Twitch.OnChatCleared += OnChatCleared;
}
protected override void OnDisabled()
{
if (Twitch is not null)
{
Twitch.OnMessageReceived -= OnMessageReceived;
Twitch.OnMessageRemoved -= OnMessageRemoved;
Twitch.OnChatCleared -= OnChatCleared;
Twitch.Dispose();
}
}
protected override void OnUpdate()
{
Panel.Style.BackgroundColor = BackgroundColor;
Panel.Style.FontColor = TextColor;
Panel.Style.FontSize = TextSize;
Panel.Style.FontFamily = FontFamily;
Panel.Style.RowGap = GapSize;
Panel.Style.Padding = Padding;
Panel.Style.FlexDirection = ReverseOrder ? FlexDirection.ColumnReverse : FlexDirection.Column;
Panel.Style.JustifyContent = Justify.FlexEnd;
if (ChatSize == 0)
{
Panel.Style.Width = Length.Auto;
Panel.Style.Height = Length.Auto;
}
else
{
Panel.Style.Width = Length.Pixels(ChatSize.x);
Panel.Style.Height = Length.Pixels(ChatSize.y);
}
Panel.Style.MaxWidth = Panel.Style.Width;
Panel.Style.MaxHeight = Panel.Style.Height;
if (ChatPosition.x < 50f)
{
Panel.Style.Left = Length.Percent(ChatPosition.x);
Panel.Style.Right = default;
}
else
{
Panel.Style.Left = default;
Panel.Style.Right = Length.Percent((100f - ChatPosition.x));
}
if (ChatPosition.y < 50f)
{
Panel.Style.Top = Length.Percent(ChatPosition.y);
Panel.Style.Bottom = default;
}
else
{
Panel.Style.Top = default;
Panel.Style.Bottom = Length.Percent((100f - ChatPosition.y));
}
}
void OnMessageReceived(TwitchChatMessage message)
{
var entry = Panel.AddChild<TwitchChatEntry>();
entry.ChatMessage = message;
entry.Lifetime = MessageTime;
entry.HasAvatarImages = HasAvatarImages;
}
void OnMessageRemoved(TwitchChatMessage message)
{
foreach (var child in Panel.Children)
{
if (child is TwitchChatEntry entry && entry.ChatMessage == message)
{
entry.Delete();
break;
}
}
}
void OnChatCleared()
{
Panel.DeleteChildren();
}
protected override int BuildHash() => System.HashCode.Combine(Username);
}@using System
@using System.Collections.Generic
@using System.Linq
@using Sandbox
@using Sandbox.UI
@namespace Skafinity
@inherits PanelComponent
@*
The optional drop-in settings panel for the Skafinity music engine.
Add this PanelComponent to a GameObject under a ScreenPanel (or WorldPanel). It finds a
SkafinityPlayer in the scene (or set Player explicitly) and offers the whole knob surface
as UI — you don't have to wire anything. A floating ♪ button toggles the board open/closed.
The engine needs nothing from this: SkafinityPlayer plays on its own. This panel is pure
convenience for players who want to tweak the vibe rather than tune it in the inspector.
The vibe editor is driven entirely from the library's VibeCodec field metadata for the
current genre: each field reports its voice (matrix row, or null for a GLOBAL knob) and
column (0 volume / 1 tone / 2 character / 3 extra). A new genre — or a new knob — is a pure
engine change; there is no field table here. s&box has no slider widget, so each knob is a
strip of tick cells (one per base-36 level the seed encodes); each change re-encodes the
vibe and restarts the player on a short debounce.
Re-theming: the palette lives as SCSS variables at the top of SkafinityMusicPanel.razor.scss.
Override those (or supply your own panel against the same SkafinityPlayer API) to restyle.
*@
<root class="@( IsOpen ? "open" : "" )">
@if ( !IsOpen )
{
<div class="fab" onclick="@Toggle">♪</div>
}
else
{
@{ var cfg = Player?.EffectiveConfig(); int genre = cfg?.Genre ?? 0; }
<div class="music">
<div class="header">
<div class="title">MUSIC</div>
<div class="close" onclick="@Toggle">✕</div>
</div>
<div class="divider"></div>
<div class="top">
<div class="row">
<div class="label">NOW PLAYING</div>
<div class="play-row">
<div class="seed-box">@Seed()</div>
<div class="btn" onclick="@CopySeed">@_copyLabel</div>
</div>
</div>
<div class="row">
<div class="label">SONG</div>
<div class="play-row">
<div class="btn wide" onclick="@( () => Step( -1 ) )">◀ Prev</div>
<div class="num">@( Player?.N ?? 0 )</div>
<div class="btn wide" onclick="@( () => Step( 1 ) )">Next ▶</div>
</div>
</div>
<div class="row">
<div class="label">PLAY A SEED</div>
<div class="play-row">
<TextEntry @ref="_tagEntry" placeholder="Paste vibe:tag:n (or a tag — blank = default)" class="tag-input" />
<div class="btn" onclick="@Play">Play</div>
<div class="btn" onclick="@UseDefault">Use default</div>
</div>
</div>
<div class="row">
<div class="label">MUTE</div>
<div class="play-row">
<div class="btn toggle @( Player?.Enabled == false ? "on" : "" )" onclick="@ToggleMute">@( Player?.Enabled == false ? "MUTED" : "PLAYING" )</div>
</div>
</div>
<div class="row">
<div class="label">VOLUME</div>
<div class="cells">
@foreach ( var v in VolumeSteps )
{
var vv = v;
<div class="cell @( ( Player?.Volume ?? 1f ) >= vv - 0.001f ? "filled" : "" )"
onclick="@( () => SetVolume( vv ) )">@VolLabel( vv )</div>
}
</div>
</div>
</div>
<div class="divider"></div>
<div class="row genre-row">
<div class="label">GENRE</div>
<div class="cells">
@for ( int g = 0; g < VibeCodec.GenreCount; g++ )
{
var gg = g;
<div class="cell choice @( g == genre ? "selected" : "" )"
onclick="@( () => SetGenre( gg ) )">@VibeCodec.Genres[g]</div>
}
<div class="btn reroll" onclick="@Reroll">🎲 Reroll</div>
<div class="btn reroll toggle @( Player?.RandomEverySong == true ? "on" : "" )"
onclick="@ToggleRandomEverySong">🎲 Random every song: @( Player?.RandomEverySong == true ? "ON" : "OFF" )</div>
<div class="btn" onclick="@Save">Save .wav</div>
</div>
</div>
<div class="label">VIBE — per-instrument mixer (tweak, then share the seed)</div>
<div class="matrix">
<div class="mrow mhead">
<div class="mvoice"></div>
@foreach ( var h in ColHeaders )
{
<div class="mcell mlabel">@h</div>
}
</div>
@foreach ( var (voice, cells) in InstrumentRows( genre ) )
{
<div class="mrow">
<div class="mvoice">@voice</div>
@for ( int col = 0; col < ColHeaders.Length; col++ )
{
var f = cells[col];
<div class="mcell">
@if ( f != null )
{
@Knob( f, cfg, f.Name == ColHeaders[col] ? "" : f.Name )
}
</div>
}
</div>
}
</div>
<div class="divider"></div>
<div class="label">GLOBAL</div>
<div class="vibe-grid">
@foreach ( var f in GlobalRows( genre ) )
{
<div class="vibe">@Knob( f, cfg, f.Name )</div>
}
</div>
@if ( _msg != null )
{
<div class="hint ok">@_msg</div>
}
</div>
}
</root>
@code
{
/// <summary>The player this panel drives. Leave unset to auto-find a <see cref="SkafinityPlayer"/>
/// in the scene on start.</summary>
[Property] public SkafinityPlayer Player { get; set; }
// One tick cell per base-36 level the seed encodes (VibeCodec.Levels).
static int UiTicks => VibeCodec.Levels;
static readonly string[] ColHeaders = { "VOLUME", "TONE", "CHARACTER", "EXTRA" };
// Volume control steps over the player's 0..2 range.
static readonly float[] VolumeSteps = { 0f, 0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f };
/// <summary>Whether the settings board is showing (toggled by the ♪ button).</summary>
public bool IsOpen { get; private set; }
TextEntry _tagEntry;
bool _tagInit;
string _copyLabel = "Copy";
string _msg;
protected override void OnStart()
{
Player ??= Scene.GetAllComponents<SkafinityPlayer>().FirstOrDefault();
if ( Player == null )
Log.Warning( "SkafinityMusicPanel: no SkafinityPlayer found in the scene — add one (or set Player)." );
}
protected override void OnUpdate()
{
// One-time seed of the text entry with the current tag once the board opens.
if ( !IsOpen ) { _tagInit = false; return; }
if ( !_tagInit && _tagEntry != null ) { _tagEntry.Text = Player?.Tag ?? ""; _tagInit = true; }
}
string Seed() => Player?.CurrentSeed ?? "—";
/// <summary>Open/close the settings board.</summary>
public void Toggle()
{
IsOpen = !IsOpen;
if ( !IsOpen ) _tagInit = false;
}
static string VolLabel( float v ) => v <= 0f ? "0" : $"{(int)Math.Round( v * 100 )}%";
// Instrument rows for the genre: each voice with its [vol, tone, character, extra] cells
// (null where a genre leaves a column empty). Order = the library's display/wire order.
static IEnumerable<(string Voice, VibeCodec.Field[] Cells)> InstrumentRows( int genre )
{
var order = new List<string>();
var byVoice = new Dictionary<string, VibeCodec.Field[]>();
foreach ( var f in VibeCodec.Fields( genre ) )
{
if ( f.Voice == null ) continue;
if ( !byVoice.TryGetValue( f.Voice, out var cells ) )
{
cells = new VibeCodec.Field[ColHeaders.Length];
byVoice[f.Voice] = cells;
order.Add( f.Voice );
}
if ( f.Column >= 0 && f.Column < cells.Length ) cells[f.Column] = f;
}
foreach ( var v in order ) yield return (v, byVoice[v]);
}
static IEnumerable<VibeCodec.Field> GlobalRows( int genre ) =>
VibeCodec.Fields( genre ).Where( f => f.Voice == null );
// Index of a field within the current genre's field list (what Player.SetVibe expects).
static int FieldIndex( int genre, VibeCodec.Field field )
{
var fields = VibeCodec.Fields( genre );
for ( int i = 0; i < fields.Count; i++ )
if ( ReferenceEquals( fields[i], field ) ) return i;
return -1;
}
// One knob: a name/value header over a strip of tick cells (or labeled choice cells).
RenderFragment Knob( VibeCodec.Field f, MusicGen.Config cfg, string label )
{
return @<text>
<div class="vibe-head">
<div class="vibe-name">@label</div>
<div class="vibe-val">@( cfg != null ? f.Display( cfg ) : "" )</div>
</div>
<div class="cells">
@{
int genre = cfg?.Genre ?? 0;
int idx = FieldIndex( genre, f );
}
@if ( f.Choices != null )
{
int sel = cfg != null ? (int)MathF.Round( f.GetNorm( cfg ) * (f.Choices.Length - 1) ) : 0;
@for ( int k = 0; k < f.Choices.Length; k++ )
{
var kk = k; var n = f.Choices.Length;
<div class="cell choice @( k == sel ? "selected" : "" )"
onclick="@( () => SetVibe( idx, kk / (float)(n - 1) ) )">@f.Choices[k]</div>
}
}
else
{
int sel = cfg != null ? (int)MathF.Round( f.GetNorm( cfg ) * (UiTicks - 1) ) : 0;
@for ( int k = 0; k < UiTicks; k++ )
{
var kk = k;
<div class="cell tick @( k <= sel ? "filled" : "" ) @( k == sel ? "selected" : "" )"
onclick="@( () => SetVibe( idx, kk / (float)(UiTicks - 1) ) )"></div>
}
}
</div>
</text>;
}
void Step( int d ) { Player?.StepN( d ); _msg = null; }
void ToggleMute() { if ( Player != null ) Player.Enabled = !Player.Enabled; }
void SetVolume( float v ) { if ( Player != null ) Player.Volume = v; }
void SetVibe( int index, float norm )
{
if ( index < 0 ) return;
Player?.SetVibe( index, norm );
_msg = null;
}
void SetGenre( int genre ) { Player?.SetGenre( genre ); _msg = null; }
void Play()
{
Player?.PlaySeed( _tagEntry?.Text );
_msg = Player != null ? $"Playing {Player.CurrentSeed}" : null;
}
void UseDefault()
{
Player?.SetTag( "" );
if ( _tagEntry != null ) _tagEntry.Text = "";
_msg = "Back to the default tag and vibe";
}
void CopySeed()
{
try { Clipboard.SetText( Seed() ); _copyLabel = "Copied!"; }
catch { _copyLabel = "—"; }
}
void Save()
{
var name = Player?.SaveCurrentToFile();
_msg = string.IsNullOrEmpty( name ) ? "Couldn't save song" : $"Saved {name} to your s&box data folder";
}
void Reroll() { Player?.RerollVibe(); _msg = "Rerolled every knob but the volumes"; }
void ToggleRandomEverySong()
{
if ( Player == null ) return;
bool on = !Player.RandomEverySong;
Player.RandomEverySong = on;
if ( on ) Player.RerollVibe();
_msg = on ? "Shuffle: every new song re-rolls the vibe (keeps your volumes)" : "Shuffle off";
}
protected override int BuildHash() =>
HashCode.Combine(
IsOpen, Player?.CurrentSeed, Player?.CurrentVibe, Player?.Enabled ?? true,
Player?.Volume ?? 1f, Player?.RandomEverySong ?? false, _msg, _copyLabel );
}
@using Sandbox.UI;
@namespace Duccsoft
@inherits Panel
<root class="video-texture">
@if ( ShowControls )
{
<VideoControlOverlay VideoPanel=@this AutoHide=@AutoHideControls AutoHideDelay=@AutoHideDelay/>
}
</root>
@using Sandbox
@using Sandbox.UI
@using LobbySystem
@inherits PanelComponent
@namespace LobbySystem.Examples
<root>
@if ( Dir is null )
{
<text></text>
}
else if ( Dir.MenuOpen || Dir.SuggestMenuOpen )
{
<div class="menu">
<div class="title">MULTIPLAYER LOBBY</div>
<div class="sub">@(Dir.MenuOpen ? "Choose a game mode" : "Suggest a mode to the host")</div>
<div class="row">
@for ( int i = 0; i < Dir.Modes.Count; i++ )
{
var idx = i;
<button class="mode" onclick=@(() => Dir.PickMode( idx ))>
<div class="k">@(idx + 1)</div>
<div class="n">@Dir.Modes[idx].DisplayName</div>
</button>
}
</div>
<button class="back" onclick=@(() => Dir.RequestCloseMenu())>Back to lobby [E]</button>
</div>
}
else
{
<div class="top">
@if ( Dir.RoundLive )
{
<div class="mode">@(Dir.ActiveMode?.DisplayName ?? "")</div>
<div class="timer @(Dir.TimeLeftSeconds < 15 ? "urgent" : "")">@TimerText</div>
}
else
{
<div class="lobby">LOBBY</div>
}
</div>
@if ( !Dir.RoundLive && !string.IsNullOrEmpty( Dir.StatusMessage ) )
{
<div class="status">@Dir.StatusMessage</div>
}
@if ( Dir.ChatVisible )
{
<div class="chat">@Dir.ChatLine</div>
}
<div class="hints">
<span>[WASD] Move</span>
<span>[Space] Jump</span>
@if ( !Dir.RoundLive )
{
<span>Walk to the pad and press [E] to start a round</span>
}
</div>
}
</root>
@code
{
LobbyDirector Dir => LobbyDirector.Current;
string TimerText
{
get
{
int t = Dir?.TimeLeftSeconds ?? 0;
if ( t < 0 ) t = 0;
return $"{t / 60:D2}:{t % 60:D2}";
}
}
protected override int BuildHash() => System.HashCode.Combine(
Dir?.State, Dir?.MenuOpen, Dir?.SuggestMenuOpen, Dir?.RoundLive,
Dir?.TimeLeftSeconds, Dir?.StatusMessage, Dir?.ChatLine, Dir?.ChatVisible );
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace TwitchAPI.Examples
<root>
</root>
@code
{
/// <summary>
/// The username of the Twitch Chat to connect to.
/// </summary>
[Property] string Username { get; set; }
/// <summary>
/// Whether or not to include the user's avatar images in the chat.
/// </summary>
[Property] bool HasAvatarImages { get; set; } = false;
/// <summary>
/// How long a message should be displayed for (in seconds).
/// </summary>
[Property, Group("Timing")] float MessageTime { get; set; } = 30f;
/// <summary>
/// How long a message should take to fade out (in seconds).
/// </summary>
[Property, Group("Timing")] float FadeTime { get; set; } = 0.5f;
/// <summary>
/// The position of the chat on the screen.
/// </summary>
[Property, Group("Styling")] Vector2 ChatPosition { get; set; } = new(5f, 95f);
/// <summary>
/// The size of the chat. Set to (0, 0) to scale automatically.
/// </summary>
[Property, Group("Styling")] Vector2 ChatSize { get; set; } = new(400f, 600f);
/// <summary>
/// The background color of the chat.
/// </summary>
[Property, Group("Styling")] Color BackgroundColor { get; set; } = Color.Transparent;
/// <summary>
/// The color of the text in the chat.
/// </summary>
[Property, Group("Styling")] Color TextColor { get; set; } = Color.White;
/// <summary>
/// The size of the text in the chat.
/// </summary>
[Property, Group("Styling")] float TextSize { get; set; } = 16f;
/// <summary>
/// The font family to use for the text in the chat.
/// </summary>
[Property, Group("Styling")] string FontFamily { get; set; } = "Poppins";
/// <summary>
/// The size of the gaps between messages in the chat.
/// </summary>
[Property, Group("Styling")] float GapSize { get; set; } = 8f;
/// <summary>
/// The padding around messages in the chat.
/// </summary>
[Property, Group("Styling")] float Padding { get; set; } = 8f;
/// <summary>
/// Whether or not to reverse the order of messages in the chat.
/// </summary>
[Property, Group("Styling")] bool ReverseOrder { get; set; } = false;
/// <summary>
/// Whether or not to use custom images for subscriber badges.
/// </summary>
[Property, Group("Badges")] bool HasCustomSubscriberBadges { get; set; } = false;
/// <summary>
/// The custom subscriber badges to use.
/// </summary>
[Property, Group("Badges"), InlineEditor, ShowIf(nameof(TwitchChatExample.HasCustomSubscriberBadges), true)] CustomSubscriberBadges SubscriberBadges { get; set; } = new();
TwitchChatConnection Twitch;
protected override void OnEnabled()
{
if (string.IsNullOrWhiteSpace(Username)) return;
Twitch = new TwitchChatConnection(Username);
Twitch.OnMessageReceived += OnMessageReceived;
Twitch.OnMessageRemoved += OnMessageRemoved;
Twitch.OnChatCleared += OnChatCleared;
}
protected override void OnDisabled()
{
if (Twitch is not null)
{
Twitch.OnMessageReceived -= OnMessageReceived;
Twitch.OnMessageRemoved -= OnMessageRemoved;
Twitch.OnChatCleared -= OnChatCleared;
Twitch.Dispose();
}
}
protected override void OnUpdate()
{
Panel.Style.BackgroundColor = BackgroundColor;
Panel.Style.FontColor = TextColor;
Panel.Style.FontSize = TextSize;
Panel.Style.FontFamily = FontFamily;
Panel.Style.RowGap = GapSize;
Panel.Style.Padding = Padding;
Panel.Style.FlexDirection = ReverseOrder ? FlexDirection.ColumnReverse : FlexDirection.Column;
Panel.Style.JustifyContent = Justify.FlexEnd;
if (ChatSize == 0)
{
Panel.Style.Width = Length.Auto;
Panel.Style.Height = Length.Auto;
}
else
{
Panel.Style.Width = Length.Pixels(ChatSize.x);
Panel.Style.Height = Length.Pixels(ChatSize.y);
}
Panel.Style.MaxWidth = Panel.Style.Width;
Panel.Style.MaxHeight = Panel.Style.Height;
if (ChatPosition.x < 50f)
{
Panel.Style.Left = Length.Percent(ChatPosition.x);
Panel.Style.Right = default;
}
else
{
Panel.Style.Left = default;
Panel.Style.Right = Length.Percent((100f - ChatPosition.x));
}
if (ChatPosition.y < 50f)
{
Panel.Style.Top = Length.Percent(ChatPosition.y);
Panel.Style.Bottom = default;
}
else
{
Panel.Style.Top = default;
Panel.Style.Bottom = Length.Percent((100f - ChatPosition.y));
}
}
void OnMessageReceived(TwitchChatMessage message)
{
var entry = Panel.AddChild<TwitchChatEntry>();
entry.ChatMessage = message;
entry.Lifetime = MessageTime;
entry.HasAvatarImages = HasAvatarImages;
}
void OnMessageRemoved(TwitchChatMessage message)
{
foreach (var child in Panel.Children)
{
if (child is TwitchChatEntry entry && entry.ChatMessage == message)
{
entry.Delete();
break;
}
}
}
void OnChatCleared()
{
Panel.DeleteChildren();
}
protected override int BuildHash() => System.HashCode.Combine(Username);
}