UI/GameMenu/GameMenu.razor.cs
using Sandbox.Diagnostics;
using Sandbox.UI;
using System;
using System.IO;
using System.IO.Compression;
namespace GuessIt;
public enum LobbyState
{
WaitingForPlayers,
ChoosingWord,
Playing,
Results
}
public partial class GameMenu : PanelComponent, Component.INetworkListener
{
[Sync] public LobbyState LobbyState { get; set; } = LobbyState.WaitingForPlayers;
[Sync] public Player CurrentlyDrawing { get; set; }
[Sync] public List<Player> RemainingPlayers { get; set; } = new();
public List<Player> AllPlayers => Scene.GetAllComponents<Player>().ToList();
public List<Player> CorrectPlayers => AllPlayers.Where( x => x.HasGuessed ).ToList();
[Sync] public string CurrentWord { get; set; } = "";
[Sync] public float CurrentDifficulty { get; set; } = 1f;
[Sync] public int CurrentRound { get; set; } = 0;
[Sync] public int TotalRounds { get; set; } = 2;
[Sync] public TimeUntil GameTimer { get; set; } = 0f;
[Sync] public bool HasDrawn { get; set; } = false;
[Sync] public bool RoundEnded { get; set; } = false;
[Sync] public TimeSince TimeSinceLetterReveal { get; set; } = 0f;
public Texture Canvas { get; set; }
public float RoundLength => 90f;
public float ResultsLength => 15f;
public float PickWordLength => 15f;
public int BrushSize = 2;
public Color BrushColor = Color.Black;
GameHeader Header { get; set; }
Panel PlayerList { get; set; }
Panel CanvasContainer { get; set; }
GameCanvas CanvasPanel { get; set; }
Toolbar CanvasToolbar { get; set; }
Panel ChatBox { get; set; }
TextEntry ChatEntry { get; set; }
GameResults ResultsPanel { get; set; }
int _clockIndex = 0;
float _previousTime = 0f;
public static GameMenu Instance { get; set; }
protected override void OnStart()
{
base.OnStart();
Instance = this;
CurrentRound = 0;
TotalRounds = 2;
}
protected override void OnUpdate()
{
if ( !Networking.IsHost ) return;
if ( LobbyState == LobbyState.Playing || LobbyState == LobbyState.ChoosingWord )
{
float seconds = MathF.Floor( MathF.Max( GameTimer, 0 ) );
if ( seconds != _previousTime )
{
Sound.Play( "ui.clock.tick" + (_clockIndex + 1) );
_clockIndex = (_clockIndex + 1) % 2;
_previousTime = seconds;
}
}
if ( LobbyState == LobbyState.Playing )
{
if ( GameTimer <= 0 )
{
if ( RoundEnded ) StartNextRound();
else EndRound();
}
else if ( !HasDrawn && CorrectPlayers.Count == 0 && GameTimer <= (RoundLength - 15) )
{
EndRound();
}
else if ( TimeSinceLetterReveal > 25f )
{
int index = Random.Shared.Int( 0, CurrentWord.Length - 1 );
BroadcastRevealLetter( index );
TimeSinceLetterReveal = 0f;
}
}
if ( LobbyState == LobbyState.Results )
{
if ( GameTimer <= 0 )
{
ResetGame();
}
}
}
protected override void OnDestroy()
{
Canvas?.Dispose();
}
protected override void OnTreeFirstBuilt()
{
base.OnTreeFirstBuilt();
foreach ( var queuedMessage in ChatQueue )
{
CreateChatEntry( queuedMessage.Item1, queuedMessage.Item2, queuedMessage.Item3 );
}
ChatQueue.Clear();
}
public void ResetGame()
{
Assert.True( Networking.IsHost );
CurrentRound = 0;
CurrentlyDrawing = null;
LobbyState = LobbyState.WaitingForPlayers;
foreach ( var player in AllPlayers )
{
player.HasGuessed = false;
player.Score = 0;
}
BroadcastRoundReset();
}
public void StartGame()
{
Assert.True( Networking.IsHost );
if ( LobbyState != LobbyState.WaitingForPlayers ) return;
if ( !string.IsNullOrEmpty( Header?.StartButtonClasses() ) ) return;
foreach ( var player in AllPlayers )
{
player.Score = 0;
}
StartNextRound();
}
public void EndGame()
{
Assert.True( Networking.IsHost );
if ( LobbyState == LobbyState.WaitingForPlayers || LobbyState == LobbyState.Results ) return;
LobbyState = LobbyState.Results;
GameTimer = ResultsLength;
CurrentlyDrawing = null;
RoundEnded = true;
BroadcastShowResults();
}
public void StartNextRound()
{
Assert.True( Networking.IsHost );
if ( LobbyState == LobbyState.Results ) return;
var players = AllPlayers;
if ( RemainingPlayers.Count == 0 )
{
if ( CurrentRound >= TotalRounds )
{
EndGame();
return;
}
players.Sort( ( a, b ) => (Random.Shared.Next( 0, 2 ) == 0 ? -1 : 1) );
RemainingPlayers = players.ToList();
CurrentRound++;
}
foreach ( var player in players )
{
player.HasGuessed = false;
player.BroadcastResetPose();
}
CurrentlyDrawing = RemainingPlayers[0];
if ( !(CurrentlyDrawing?.GameObject?.IsValid() ?? false) )
{
RemainingPlayers.RemoveAt( 0 );
StartNextRound();
return;
}
LobbyState = LobbyState.ChoosingWord;
RemainingPlayers.RemoveAt( 0 );
GameTimer = PickWordLength;
TimeSinceLetterReveal = 0;
RoundEnded = false;
HasDrawn = false;
BroadcastRoundStart( CurrentlyDrawing );
}
public void EndRound()
{
Assert.True( Networking.IsHost );
if ( LobbyState == LobbyState.Results || LobbyState == LobbyState.WaitingForPlayers ) return;
if ( RoundEnded ) return;
foreach ( var player in AllPlayers )
{
if ( (!player.IsDrawing && !player.HasGuessed) || (player.IsDrawing && CorrectPlayers.Count == 0) )
{
player.BroadcastAngryMorph();
}
}
RoundEnded = true;
GameTimer = 5;
BroadcastRevealAnswer( CurrentWord, CorrectPlayers.Count > 0 );
CurrentWord = "";
}
[Rpc.Broadcast( NetFlags.HostOnly )]
private void StartDrawing( string word )
{
LobbyState = LobbyState.Playing;
if ( Player.Local.IsDrawing )
{
Header.WordPanel.SetWord( word, "You are drawing:", true );
}
else
{
Header.WordPanel.SetWord( word );
}
}
public void SubmitGuess()
{
if ( string.IsNullOrEmpty( ChatEntry.Text ) ) return;
BroadcastSubmitGuess( ChatEntry.Text );
ChatEntry.Text = "";
ChatEntry.Focus();
}
private void CorrectGuess( Player player )
{
Assert.True( Networking.IsHost );
if ( player.HasGuessed ) return;
int DrawingScore = 200;
int PlayerScore = (int)MathF.Floor( MathX.Remap( GameTimer, RoundLength, 0, 1000, 800 ) );
if ( CorrectPlayers.Count == 0 )
{
CurrentlyDrawing.BroadcastHappyMorph();
}
player.HasGuessed = true;
player.BroadcastHappyMorph();
if ( CorrectPlayers.Count == AllPlayers.Count - 1 )
{
if ( CorrectPlayers.Count > 1 )
{
PlayerScore = (int)MathF.Floor( MathX.Remap( GameTimer, 30, 0, 800, 250 ) );
DrawingScore = 100;
if ( CorrectPlayers.Count >= 2 )
{
AllCorrect();
}
}
EndRound();
}
else if ( CorrectPlayers.Count == 1 )
{
if ( GameTimer > 30f ) GameTimer = 30f;
}
else
{
if ( CorrectPlayers.Count >= MathF.Floor( (AllPlayers.Count - 1) / 2 ) )
{
if ( GameTimer > 15f ) GameTimer = 15f;
}
PlayerScore = (int)MathF.Floor( MathX.Remap( GameTimer, 30, 0, 1000, 200 ) );
DrawingScore = 100;
}
CurrentlyDrawing.Score += (int)MathF.Floor( DrawingScore * CurrentDifficulty );
player.Score += (int)MathF.Floor( PlayerScore * CurrentDifficulty );
}
private void AllCorrect()
{
using ( Rpc.FilterInclude( x => x.Id == CurrentlyDrawing.Network.OwnerId ) )
{
BroadcastAllCorrect();
}
}
public void SendCanvasToUser( Connection connection )
{
if ( !(Canvas?.IsValid ?? false) ) return;
var colors = Canvas.GetPixels();
var bytes = new List<byte>();
foreach ( var color in colors )
{
bytes.Add( color.r );
bytes.Add( color.g );
bytes.Add( color.b );
bytes.Add( color.a );
}
using var ms = new MemoryStream();
using ( var zs = new GZipStream( ms, CompressionMode.Compress ) )
{
var data = bytes.ToArray();
zs.Write( data, 0, data.Length );
}
var str = Convert.ToBase64String( ms.ToArray() );
using ( Rpc.FilterInclude( x => x.Id == connection.Id ) )
{
BroadcastCanvas( str );
}
}
[Rpc.Host]
public void ChooseWord( string word, int difficulty )
{
var callingPlayer = AllPlayers.FirstOrDefault( x => x.Network.OwnerId == Rpc.CallerId );
if ( !callingPlayer.IsDrawing ) return;
CurrentWord = word;
CurrentDifficulty = (new float[] { 0.9f, 1.0f, 1.1f })[difficulty];
GameTimer = RoundLength;
StartDrawing( CurrentWord );
}
public void Draw( Vector2 point, Color color, int size )
{
if ( Canvas is null ) { Log.Warning( "[GameMenu] Draw: Canvas is null" ); return; }
if ( LobbyState != LobbyState.Playing ) { Log.Warning( $"[GameMenu] Draw: wrong state {LobbyState}" ); return; }
if ( RoundEnded ) { Log.Warning( "[GameMenu] Draw: round ended" ); return; }
HasDrawn = true;
// Draw a circle of radius size at point with color color
Canvas.Update( color.ToColor32(), new Rect( point.x - size, point.y - size, size * 2, size * 2 ) );
CanvasPanel?.MarkDirty();
}
public async void Draw( List<Vector2> points, Color color, int size, bool delay = false )
{
if ( Canvas is null ) return;
if ( LobbyState != LobbyState.Playing ) return;
float totalTime = 50f; // ms
float timePerPoint = totalTime / points.Count;
// Draw lines of radius size between each point with color color
for ( int i = 0; i < points.Count - 1; i++ )
{
Vector2 point1 = points[i];
Vector2 point2 = points[i + 1];
Vector2 diff = point2 - point1;
float length = diff.Length;
float timePerThing = timePerPoint / length;
for ( int j = 0; j < length; j++ )
{
Vector2 point = point1 + (diff.Normal * j);
Draw( point, color, size );
if ( delay ) await GameTask.Delay( (int)MathF.Floor( timePerThing ) );
}
}
}
public void OnConnected( Connection connection )
{
if ( !Networking.IsHost ) return;
BroadcastChatEntry( "", $"{connection.DisplayName} has joined the game!", "join-message" );
}
public void OnDisconnected( Connection connection )
{
if ( !Networking.IsHost ) return;
BroadcastChatEntry( "", $"{connection.DisplayName} has left the game!", "leave-message" );
if ( !CurrentlyDrawing.IsValid() || CurrentlyDrawing.Network.OwnerId == connection.Id || (CorrectPlayers.Count == AllPlayers.Count - 1) )
{
EndRound();
}
var player = AllPlayers.FirstOrDefault( x => x.Network.OwnerId == connection.Id );
if ( player.IsValid() )
{
if ( !player.HasGuessed && (AllPlayers.Count - CorrectPlayers.Count) == 2 )
{
EndRound();
}
}
}
private void ResetCanvas()
{
byte[] textureData = new byte[320 * 240 * 4];
for ( int i = 0; i < textureData.Length; i++ )
{
textureData[i] = 255;
}
// Create the new texture before disposing the old one so the Image
// panel never briefly references a disposed/error texture.
var oldCanvas = Canvas;
Canvas = Texture.Create( 320, 240 )
.WithDynamicUsage()
.WithData( textureData )
.Finish();
CanvasPanel.SetTexture( Canvas );
CanvasPanel.MarkDirty();
oldCanvas?.Dispose();
StateHasChanged();
}
public void ToggleLocalMic()
{
var localPlayer = Player.Local;
if ( !localPlayer.IsValid() ) return;
localPlayer.Voice.IsListening = !localPlayer.Voice.IsListening;
}
[Rpc.Host]
public void RequestCanvas()
{
SendCanvasToUser( Rpc.Caller );
}
[Rpc.Broadcast( NetFlags.HostOnly )]
public void BroadcastCanvas( string compressedStr )
{
ResetCanvas();
Header.WordPanel.SetWord( CurrentWord );
byte[] bytes;
try
{
using var ms = new MemoryStream( Convert.FromBase64String( compressedStr ) );
using var zs = new GZipStream( ms, CompressionMode.Decompress );
using var outStream = new MemoryStream();
zs.CopyTo( outStream );
bytes = outStream.ToArray();
}
catch
{
Log.Warning( "Failed to decompress canvas data" );
return;
}
var colors = new List<Color32>();
var size = bytes.Count() / 4;
for ( int i = 0; i < size; i++ )
{
var index = i * 4;
colors.Add( new Color32( bytes[index], bytes[index + 1], bytes[index + 2], bytes[index + 3] ) );
}
Canvas.Update( colors.ToArray() );
}
[Rpc.Broadcast( NetFlags.HostOnly )]
public void BroadcastRoundReset()
{
if ( Networking.IsHost )
{
Header.WordPanel.SetOverride( AllPlayers.Count <= 1 ? "Waiting for players..." : "Waiting for host to begin..." );
}
else
{
Header.WordPanel.SetOverride( "Waiting for host to begin..." );
}
ResultsPanel?.Delete();
ResultsPanel = null;
}
[Rpc.Broadcast( NetFlags.HostOnly )]
public void BroadcastAllCorrect()
{
Sandbox.Services.Stats.Increment( "all-correct-while-drawing", 1 );
}
[Rpc.Broadcast( NetFlags.HostOnly )]
public void BroadcastRoundStart( Player currentlyDrawing )
{
ResetCanvas();
ResultsPanel?.Delete();
ResultsPanel = null;
if ( Player.Local == currentlyDrawing )
{
Header.WordPanel.SetOverride( "Choose a word!" );
CanvasContainer.AddChild<WordSelection>();
}
else
{
Header.WordPanel.SetOverride( $"{currentlyDrawing?.Network?.Owner?.DisplayName ?? "someone"} is choosing a word..." );
}
}
[Rpc.Broadcast]
public void BroadcastRevealAnswer( string answer, bool hadGuesses = true )
{
if ( (Player.Local?.IsDrawing ?? false) && hadGuesses || (Player.Local?.HasGuessed ?? false) )
{
Sound.Play( "ui.reveal.correct" );
}
else
{
Sound.Play( "ui.reveal.wrong" );
}
CreateChatEntry( "", "The word was: " + answer, "reveal-word" );
Header?.WordPanel?.SetWord( answer, "The word was:", true );
}
[Rpc.Broadcast( NetFlags.HostOnly )]
public void BroadcastRevealLetter( int index )
{
Header?.WordPanel?.RevealLetter( index );
}
[Rpc.Broadcast]
public void BroadcastDraw( Vector2 point, Color color, int size )
{
var callingPlayer = AllPlayers.FirstOrDefault( x => x.Network.OwnerId == Rpc.CallerId );
if ( !callingPlayer.IsDrawing ) return;
Draw( point, color, size );
}
[Rpc.Broadcast]
public void BroadcastDraw( List<Vector2> points, Color color, int size )
{
var callingPlayer = AllPlayers.FirstOrDefault( x => x.Network.OwnerId == Rpc.CallerId );
if ( !callingPlayer.IsDrawing ) return;
Draw( points, color, size );
}
[Rpc.Broadcast( NetFlags.HostOnly )]
public void BroadcastShowResults()
{
ResetCanvas();
Header.WordPanel.SetOverride( "Game Results" );
Dictionary<Player, int> leaderboard = new();
foreach ( var player in AllPlayers )
{
leaderboard[player] = player.Score;
}
leaderboard = leaderboard.OrderByDescending( x => x.Value ).ToDictionary( x => x.Key, x => x.Value );
GameResults results = CanvasContainer.AddChild<GameResults>();
results.Winner = AllPlayers.OrderByDescending( x => x.Score ).FirstOrDefault();
results.Leaderboard = leaderboard;
ResultsPanel = results;
if ( results.Winner == Player.Local )
{
Sandbox.Services.Stats.Increment( "games-won", 1 );
if ( AllPlayers.Count >= 4 )
{
Sandbox.Services.Achievements.Unlock( "winner" );
}
}
}
[Rpc.Broadcast]
public void BroadcastSubmitGuess( string message )
{
var player = AllPlayers.FirstOrDefault( x => x.Network.OwnerId == Rpc.CallerId );
if ( player is null ) return;
if ( player.IsDrawing && !RoundEnded )
{
if ( player == Player.Local )
{
CreateChatEntry( "", "You can't chat while you're drawing!", "is-drawing" );
}
return;
}
var localPlayer = Player.Local;
bool shouldSendMessage = true;
if ( LobbyState != LobbyState.WaitingForPlayers && LobbyState != LobbyState.Results )
{
// Strip trailing punctuation so guesses like "dog!" or "cat?" still match
var trimmedMessage = message.TrimEnd( '.', '!', '?', ',', ';', ':' );
var parts = trimmedMessage.Split( ' ' );
var hasPart = parts.Any( x => string.Equals( x.TrimEnd( '.', '!', '?', ',', ';', ':' ), CurrentWord, StringComparison.OrdinalIgnoreCase ) );
var hasString = trimmedMessage.Contains( CurrentWord, StringComparison.OrdinalIgnoreCase );
if ( !player.HasGuessed && ((CurrentWord.Contains( ' ' ) && hasString) || hasPart) && !string.IsNullOrEmpty( CurrentWord ) )
{
// Capture the word now, before CorrectGuess -> EndRound can clear CurrentWord
var correctWord = CurrentWord;
if ( Networking.IsHost && !RoundEnded ) CorrectGuess( player );
if ( player == localPlayer )
{
Sandbox.Services.Stats.Increment( "correct-guesses", 1 );
Header.WordPanel.SetWord( correctWord, "The word was:", true );
}
else if ( localPlayer.IsDrawing ) Sandbox.Services.Stats.Increment( "correct-while-drawing", 1 );
CreateChatEntry( player.Network.Owner.DisplayName, "guessed correctly!", "guess-correct" );
Sound.Play( "ui.guess.correct" );
shouldSendMessage = false;
}
if ( player.HasGuessed && !localPlayer.HasGuessed )
{
shouldSendMessage = false;
}
}
if ( shouldSendMessage )
{
CreateChatEntry( player.Network.Owner.DisplayName, message, player.HasGuessed ? "post-game" : "" );
}
}
[Rpc.Broadcast]
public void BroadcastChatEntry( string name, string message, string classes = "" )
{
CreateChatEntry( name, message, classes );
}
int ChatIndex = 0;
List<(string, string, string)> ChatQueue = new();
public void CreateChatEntry( string name, string message, string classes = "" )
{
if ( !ChatBox.IsValid() )
{
ChatQueue.Add( (name, message, classes) );
return;
}
var entry = ChatBox.AddChild<ChatEntry>();
entry.SetMessage( name, message );
entry.AddClass( classes );
if ( ChatBox.ChildrenCount > 250 )
{
ChatBox.GetChild( 0 ).Delete();
}
Sound.Play( "ui.chat.message" + (ChatIndex + 1) );
ChatIndex = (ChatIndex + 1) % 2;
}
string LogoClass()
{
return "";
}
}