Compare commits

9 Commits

25 changed files with 962 additions and 98 deletions

15
.sbproj
View File

@@ -37,7 +37,20 @@
"GameSettings": {}, "GameSettings": {},
"Addons": "", "Addons": "",
"PreLaunchCommand": "", "PreLaunchCommand": "",
"PostLaunchCommand": "" "PostLaunchCommand": "lucker_minigames_per_round 1"
}
],
"PackageSettings": [
{
"DisplayType": "Integer",
"Choices": [],
"ConVarName": "lucker_minigames_per_round",
"DisplayName": "Minigames Per Round",
"DefaultValue": "1",
"Description": "The number of minigames played per round",
"Group": "Other",
"Minimum": 1,
"Maximum": 12
} }
] ]
} }

View File

@@ -1,22 +1,39 @@
using LuckerGame.Components.Lucker.Cameras; using LuckerGame.Components.Lucker.Cameras;
using LuckerGame.EntityComponents.Lucker;
using LuckerGame.Events; using LuckerGame.Events;
using Sandbox; using Sandbox;
namespace LuckerGame.Entities; namespace LuckerGame.Entities;
/// <summary> /// <summary>
/// Represents a Player. /// Represents a person playing the game.
/// This could belong to a Client or a Bot and represents a common entity to operate on for games and keeping score /// This could belong to a Client or a Bot and represents a common entity to operate on for games and keeping score
/// </summary> /// </summary>
public partial class Lucker : Entity public partial class Lucker : Entity
{ {
/// <summary> /// <summary>
/// The entity this Player currently controls /// The entity this lucker controls. This value is networked and should be accessed through <see cref="Pawn"/>.
/// </summary> /// </summary>
public Entity Pawn { get; set; } [Net] private Entity InternalPawn { get; set; }
/// <summary> /// <summary>
/// Before the round has started, this player indicated they were ready /// Accesses or sets the entity this lucker currently controls
/// </summary>
public Entity Pawn
{
get => InternalPawn;
set
{
InternalPawn = value;
if ( value != null )
{
value.Owner = this;
}
}
}
/// <summary>
/// Before the round has started, this lucker indicated they were ready
/// </summary> /// </summary>
[Net] public bool Ready { get; set; } [Net] public bool Ready { get; set; }
@@ -25,20 +42,23 @@ public partial class Lucker : Entity
/// </summary> /// </summary>
[BindComponent] public AbstractCamera Camera { get; } [BindComponent] public AbstractCamera Camera { get; }
[BindComponent] public LuckerStats Stats { get; }
/// <summary> /// <summary>
/// Creates and properly sets up a Player entity for a given client /// Creates and properly sets up a <see cref="Lucker"/> entity for a given client
/// </summary> /// </summary>
/// <param name="client">the client to own the player</param> /// <param name="client">the client to own the lucker</param>
/// <returns>the newly created player</returns> /// <returns>the newly created lucker</returns>
public static Lucker CreateLuckerForClient( IClient client ) public static Lucker CreateLuckerForClient( IClient client )
{ {
var player = new Lucker(); var lucker = new Lucker();
client.Pawn = player; client.Pawn = lucker;
player.Owner = client as Entity; lucker.Owner = client as Entity;
player.Name = client.Name; lucker.Name = client.Name;
var camera = player.Components.Create<RTSCamera>(); lucker.Components.Create<RTSCamera>();
lucker.Components.Create<LuckerStats>();
return player; return lucker;
} }
/// <summary> /// <summary>
@@ -49,12 +69,12 @@ public partial class Lucker : Entity
public static void ReadyUpCommand(bool readyState) public static void ReadyUpCommand(bool readyState)
{ {
var client = ConsoleSystem.Caller; var client = ConsoleSystem.Caller;
var player = client.Pawn as Lucker; var lucker = client.Pawn as Lucker;
player.SetReady( readyState ); lucker.SetReady( readyState );
} }
/// <summary> /// <summary>
/// Sets this player's ready state /// Sets this lucker's ready state
/// </summary> /// </summary>
/// <param name="ready">the ready state being set</param> /// <param name="ready">the ready state being set</param>
public void SetReady(bool ready) public void SetReady(bool ready)
@@ -62,7 +82,7 @@ public partial class Lucker : Entity
Ready = ready; Ready = ready;
if ( Game.IsServer ) if ( Game.IsServer )
{ {
Event.Run( LuckerEvent.PlayerReady, this, ready ); Event.Run( LuckerEvent.LuckerReady, this, ready );
} }
} }

View File

@@ -12,8 +12,20 @@ namespace LuckerGame.Entities;
/// </summary> /// </summary>
public partial class MinigameManager : Entity public partial class MinigameManager : Entity
{ {
/// <summary>
/// The currently loaded minigame
/// </summary>
[Net] public Minigame LoadedMinigame { get; private set; } [Net] public Minigame LoadedMinigame { get; private set; }
private List<Minigame> AvailableMinigames { get; set; }
/// <summary>
/// A cached list of available minigames. Gets reloaded on a hotreload
/// </summary>
private List<TypeDescription> AvailableMinigames { get; set; }
/// <summary>
/// The luckers involved in the current minigame
/// </summary>
private List<Lucker> InvolvedLuckers { get; set; }
public override void Spawn() public override void Spawn()
{ {
@@ -21,13 +33,17 @@ public partial class MinigameManager : Entity
FindMinigames(); FindMinigames();
} }
public void StartMinigame(List<Lucker> players, string minigameName = null) public void StartMinigame(List<Lucker> luckers, string minigameName = null)
{ {
InvolvedLuckers = luckers.ToList();
if (CheckForMinigames()) if (CheckForMinigames())
{ {
LoadedMinigame = string.IsNullOrEmpty( minigameName ) ? AvailableMinigames.OrderBy( _ => Guid.NewGuid() ).FirstOrDefault() : TypeLibrary.Create<Minigame>( minigameName ); LoadedMinigame = string.IsNullOrEmpty( minigameName )
? TypeLibrary.Create<Minigame>( AvailableMinigames.OrderBy( _ => Guid.NewGuid() ).FirstOrDefault()
.TargetType )
: TypeLibrary.Create<Minigame>( minigameName );
ChatBox.AddInformation( To.Everyone, $"Starting {LoadedMinigame.Name}" ); ChatBox.AddInformation( To.Everyone, $"Starting {LoadedMinigame.Name}" );
LoadedMinigame.Initialize( players ); LoadedMinigame.Initialize( luckers );
} }
} }
@@ -46,7 +62,7 @@ public partial class MinigameManager : Entity
{ {
AvailableMinigames = TypeLibrary.GetTypes<Minigame>() AvailableMinigames = TypeLibrary.GetTypes<Minigame>()
.Where( type => !type.IsAbstract && !type.IsInterface ) .Where( type => !type.IsAbstract && !type.IsInterface )
.Select( td => TypeLibrary.Create<Minigame>( td.TargetType ) ).ToList(); .ToList();
} }
[Event.Hotload] [Event.Hotload]
@@ -55,12 +71,48 @@ public partial class MinigameManager : Entity
FindMinigames(); FindMinigames();
} }
public void Tick() /// <summary>
/// Goes through the luckers included in the loaded minigame and deletes and nulls out any pawns assigned to them
/// </summary>
private void CleanupLuckerPawns()
{
if ( LoadedMinigame is not { IsValid: true } || InvolvedLuckers == null)
{
Log.Warning( "Attempted to clean up players without a minigame loaded!" );
return;
}
InvolvedLuckers.ForEach( lucker =>
{
lucker.Pawn?.Delete();
lucker.Pawn = null;
} );
}
/// <summary>
/// Called once per tick by the RoundManager. Ticks any running minigame.
/// </summary>
/// <returns>true if the current minigame has ended, else false</returns>
public bool Tick()
{ {
if ( LoadedMinigame is not { IsValid: true } ) if ( LoadedMinigame is not { IsValid: true } )
{ {
return; return false;
} }
LoadedMinigame.Tick(); var ended = LoadedMinigame.Tick();
if ( !ended )
{
return false;
}
EndMinigame();
return true;
}
private void EndMinigame()
{
LoadedMinigame.Cleanup();
CleanupLuckerPawns();
LoadedMinigame.Delete();
LoadedMinigame = null;
InvolvedLuckers = null;
} }
} }

View File

@@ -47,11 +47,21 @@ public partial class RoundManager : Entity
#region In Progress State #region In Progress State
private const int MinigamesPerRound = 1; /// <summary>
/// The number of minigames that should be played per round, settable via a convar
/// </summary>
[ConVar.Replicated("lucker_minigames_per_round")]
private static int MinigamesPerRound { get; set; }
private int MinigamesLeftInRound { get; set; } /// <summary>
/// The number of minigames left in the current round
/// </summary>
public int MinigamesLeftInRound { get; set; }
private List<Lucker> Players { get; set; } /// <summary>
/// The luckers playing in the current round
/// </summary>
private List<Lucker> Luckers { get; set; }
#endregion #endregion
/// <inheritdoc/> /// <inheritdoc/>
@@ -76,16 +86,29 @@ public partial class RoundManager : Entity
if ( RoundState == RoundState.InProgress ) if ( RoundState == RoundState.InProgress )
{ {
MinigameManager.Tick(); var ended = MinigameManager.Tick();
if ( ended )
{
MinigamesLeftInRound--;
if ( MinigamesLeftInRound > 0 )
{
MinigameManager.StartMinigame( Luckers );
}
else
{
RoundState = RoundState.NotStarted;
}
}
} }
} }
/// <summary> /// <summary>
/// Is triggered whenever a player readies up /// Is triggered whenever a lucker readies up or readies down
/// </summary> /// </summary>
/// <param name="readyLucker">the player that readied up, discarded</param> /// <param name="readyLucker">the lucker that readied up</param>
[LuckerEvent.PlayerReady] /// <param name="ready">the lucker's ready state</param>
public void HandlePlayerReady( Lucker readyLucker, bool ready ) [LuckerEvent.LuckerReady]
public void HandleLuckerReady( Lucker readyLucker, bool ready )
{ {
if ( RoundState != RoundState.NotStarted && RoundState != RoundState.StartCountdown ) if ( RoundState != RoundState.NotStarted && RoundState != RoundState.StartCountdown )
{ {
@@ -94,9 +117,9 @@ public partial class RoundManager : Entity
Log.Info( $"{readyLucker.Client.Name} set ready to {ready}" ); Log.Info( $"{readyLucker.Client.Name} set ready to {ready}" );
var message = $"{readyLucker.Client.Name} is {(ready ? "now ready." : "no longer ready.")}"; var message = $"{readyLucker.Client.Name} is {(ready ? "now ready." : "no longer ready.")}";
ChatBox.AddInformation( To.Everyone, message ); ChatBox.AddInformation( To.Everyone, message );
var players = All.OfType<Lucker>().ToList(); var luckers = All.OfType<Lucker>().ToList();
var readiedCount = players.Count( player => player.Ready ); var readiedCount = luckers.Count( lucker => lucker.Ready );
var totalCount = players.Count; var totalCount = luckers.Count;
if ( (float)readiedCount / totalCount > RequiredReadyPercent && RoundState == RoundState.NotStarted ) if ( (float)readiedCount / totalCount > RequiredReadyPercent && RoundState == RoundState.NotStarted )
{ {
Log.Info( "Countdown started" ); Log.Info( "Countdown started" );
@@ -119,8 +142,13 @@ public partial class RoundManager : Entity
} }
RoundState = RoundState.InProgress; RoundState = RoundState.InProgress;
Players = All.OfType<Lucker>().ToList(); Luckers = All.OfType<Lucker>().ToList();
MinigameManager.StartMinigame( Players, minigameName ); Luckers.ForEach( lucker =>
{
lucker.Ready = false;
} );
MinigamesLeftInRound = MinigamesPerRound;
MinigameManager.StartMinigame( Luckers, minigameName );
} }
[ConCmd.Server( "start_round" )] [ConCmd.Server( "start_round" )]

View File

@@ -76,7 +76,7 @@ public partial class Weapon : AnimatedEntity
} }
/// <summary> /// <summary>
/// Called when the weapon is either removed from the player, or holstered. /// Called when the weapon is either removed from the pawn, or holstered.
/// </summary> /// </summary>
public void OnHolster() public void OnHolster()
{ {

View File

@@ -1,15 +0,0 @@
using Sandbox;
namespace LuckerGame.EntityComponents.Lucker;
/// <summary>
/// A component for capturing and passing around a client's input for an attached Lucker
/// </summary>
public class LuckerClientInput : EntityComponent<Entities.Lucker>
{
[ClientInput]
public Vector3 InputDirection { get; set; }
[ClientInput]
public Angles ViewAngles { get; set; }
}

View File

@@ -0,0 +1,48 @@
using Sandbox;
using Sandbox.UI;
namespace LuckerGame.EntityComponents.Lucker;
/// <summary>
/// Handles the stats associated with a lucker during a lobby.
/// </summary>
public partial class LuckerStats : EntityComponent<Entities.Lucker>, ISingletonComponent
{
/// <summary>
/// The lucker's current score.
/// </summary>
[Net] private long Score { get; set; }
/// <summary>
/// Adds points to this lucker's score
/// </summary>
/// <param name="points">points to add (or remove if negative)</param>
public void AddScore( long points )
{
Score += points;
if ( points == 0 )
{
return;
}
var message = $"{Entity.Name} {(points > 0 ? "gained" : "lost")} {points} points!";
ChatBox.AddInformation( To.Everyone, message );
}
/// <summary>
/// Resets this lucker's score to zero
/// </summary>
public void ResetScore()
{
Score = 0;
}
/// <summary>
/// Gets this lucker's current score
/// </summary>
/// <returns>this lucker's current score</returns>
public long GetScore()
{
return Score;
}
}

View File

@@ -4,16 +4,16 @@ namespace LuckerGame.Events;
public static partial class LuckerEvent public static partial class LuckerEvent
{ {
public const string PlayerReady = "lucker.playerReady"; public const string LuckerReady = "lucker.luckerReady";
/// <summary> /// <summary>
/// Event is run on the server whenever a player changes ready state /// Event is run on the server whenever a lucker changes ready state
/// The event handler is given the player that readied up and their new ready state /// The event handler is given the lucker that readied up and their new ready state
/// </summary> /// </summary>
[MethodArguments(typeof(Entities.Lucker), typeof(bool))] [MethodArguments(typeof(Entities.Lucker), typeof(bool))]
public class PlayerReadyAttribute : EventAttribute public class LuckerReadyAttribute : EventAttribute
{ {
public PlayerReadyAttribute() : base(PlayerReady) public LuckerReadyAttribute() : base(LuckerReady)
{ {
} }
} }

View File

@@ -15,13 +15,14 @@ public abstract class Minigame : Entity
/// <summary> /// <summary>
/// Initializes the minigame with a list of luckers playing it. /// Initializes the minigame with a list of luckers playing it.
/// </summary> /// </summary>
/// <param name="players">the players who made it into the minigame</param> /// <param name="luckers">the luckers who made it into the minigame</param>
public abstract void Initialize(List<Lucker> players); public abstract void Initialize(List<Lucker> luckers);
/// <summary> /// <summary>
/// Once a minigame is loaded and initialized, this method is called once per server tick. /// Once a minigame is loaded and initialized, this method is called once per server tick.
/// </summary> /// </summary>
public abstract void Tick(); /// <returns>true if the minigame has ended, false otherwise</returns>
public abstract bool Tick();
/// <summary> /// <summary>
/// Cleans up any entities and components created by this minigame. /// Cleans up any entities and components created by this minigame.

View File

@@ -41,7 +41,7 @@ public partial class RussianPistol : Weapon
} }
else else
{ {
Pawn.PlaySound( "denyundo" ); Pawn.PlaySound( "player_use_fail" );
} }
Ammo--; Ammo--;
} }

View File

@@ -14,87 +14,102 @@ namespace LuckerGame.Minigames.RussianRoulette;
public class RussianRouletteMinigame : Minigame public class RussianRouletteMinigame : Minigame
{ {
public override string Name => "Russian Roulette"; public override string Name => "Russian Roulette";
private List<Lucker> Players { get; set; } private List<Lucker> Luckers { get; set; }
private Pawn Shooter { get; set; } private Pawn Shooter { get; set; }
private const float ShooterDistance = 80f; private const float ShooterDistance = 80f;
private const float TimeBetweenShots = 7f; private const float TimeBetweenShots = 7f;
private const float TimeBetweenDeathAndEnd = 5f;
private const string ShooterName = "The Russian";
private int Taunted = 0; private int Taunted = 0;
private Pawn ShooterTarget;
private List<Pawn> DeadVictims => Players private List<Pawn> DeadVictims => Luckers
.Select( player => player.Pawn as Pawn ) .Select( lucker => lucker.Pawn as Pawn )
.Where( pawn => !pawn.IsValid || pawn.LifeState != LifeState.Alive ) .Where( pawn => pawn is not { IsValid: true } || pawn.LifeState != LifeState.Alive )
.ToList(); .ToList();
private TimeSince TimeSinceShot { get; set; } private TimeSince TimeSinceShot { get; set; }
private TimeSince TimeSinceDeadVictim { get; set; }
public override void Initialize( List<Lucker> players ) public override void Initialize( List<Lucker> luckers )
{ {
Players = players; Luckers = luckers;
Shooter = new Pawn(); Shooter = new Pawn();
Shooter.Name = ShooterName;
var shooterInventory = Shooter.Components.Create<PawnInventory>(); var shooterInventory = Shooter.Components.Create<PawnInventory>();
shooterInventory.AddWeapon( new RussianPistol() ); shooterInventory.AddWeapon( new RussianPistol() );
// Setup cameras for players // Setup cameras for luckers
Players.ForEach( player => Luckers.ForEach( lucker =>
{ {
player.Components.Create<RTSCamera>(); lucker.Components.Create<RTSCamera>();
player.Position = Shooter.Position; lucker.Position = Shooter.Position;
} ); } );
Players.Select((player, i) => (Player: player, Index: i) ).ToList().ForEach( pair => Luckers.Select((lucker, i) => (Lucker: lucker, Index: i) ).ToList().ForEach( pair =>
{ {
var player = pair.Player; var lucker = pair.Lucker;
var index = pair.Index; var index = pair.Index;
var pawn = new Pawn(); var pawn = new Pawn();
pawn.Name = player.Name; pawn.Name = lucker.Name;
pawn.Tags.Add( "victim" ); pawn.Tags.Add( "victim" );
pawn.Health = 1; pawn.Health = 1;
player.Pawn = pawn; lucker.Pawn = pawn;
pawn.DressFromClient( player.Client ); pawn.DressFromClient( lucker.Client );
var pawnOffset = ShooterDistance * (index % 2 == 0 ? Vector3.Forward : Vector3.Right) * (index % 4 >= 2 ? -1 : 1); var pawnOffset = ShooterDistance * (index % 2 == 0 ? Vector3.Forward : Vector3.Right) * (index % 4 >= 2 ? -1 : 1);
player.Pawn.Position = Shooter.Position + pawnOffset; lucker.Pawn.Position = Shooter.Position + pawnOffset;
pawn.LookAt(Shooter.Position); pawn.LookAt(Shooter.Position);
} ); } );
TimeSinceShot = 0; TimeSinceShot = 0;
Taunted = 0;
} }
public override void Tick() public override bool Tick()
{ {
// Someone is dead, we're getting ready to end
if ( DeadVictims.Any() ) if ( DeadVictims.Any() )
{ {
if ( Taunted != int.MaxValue ) if ( Taunted != int.MaxValue )
{ {
ChatBox.AddChatEntry( To.Everyone, "Shooter", "Heh, nothing personnel, kid." ); ChatBox.AddChatEntry( To.Everyone, Shooter.Name, "Heh, nothing personnel, kid." );
Taunted = int.MaxValue; Taunted = int.MaxValue;
TimeSinceDeadVictim = 0;
}
else if(TimeSinceDeadVictim > TimeBetweenDeathAndEnd)
{
return true;
} }
return;
} }
if ( TimeSinceShot > TimeBetweenShots ) else if ( TimeSinceShot > TimeBetweenShots )
{ {
TimeSinceShot = 0; TimeSinceShot = 0;
Taunted = 0; Taunted = 0;
Shooter.Inventory.ActiveWeapon.PrimaryAttack(); Shooter.Inventory.ActiveWeapon.PrimaryAttack();
if ( !DeadVictims.Any() ) if ( !DeadVictims.Any() )
{ {
ChatBox.AddChatEntry( To.Everyone, "Shooter", "Fucking lag..." ); ChatBox.AddChatEntry( To.Everyone, Shooter.Name, "Fucking lag..." );
} }
} }
else if ( TimeSinceShot > TimeBetweenShots * .8f && Taunted == 1) else if ( TimeSinceShot > TimeBetweenShots * .8f && Taunted == 1)
{ {
var victim = Players.Select( player => player.Pawn as Pawn ) ShooterTarget = Luckers.Select( lucker => lucker.Pawn as Pawn )
.OrderBy( _ => Guid.NewGuid() ) .OrderBy( _ => Guid.NewGuid() )
.FirstOrDefault(); .FirstOrDefault();
Shooter.LookAt( victim.Position ); Shooter.LookAt( ShooterTarget.Position );
ChatBox.AddChatEntry( To.Everyone, "Shooter", $"I'm gonna eat you up, {victim.Name}" ); var chance = 1f / Shooter.Inventory.ActiveWeapon.Ammo;
ChatBox.AddChatEntry( To.Everyone, Shooter.Name, $"Good luck, {ShooterTarget.Name}! You have a {chance:P0} chance to die!" );
Taunted++; Taunted++;
} }
else if ( TimeSinceShot > TimeBetweenShots / 2 && Taunted == 0) else if ( TimeSinceShot > TimeBetweenShots / 2 && Taunted == 0)
{ {
ChatBox.AddChatEntry( To.Everyone, "Shooter", "Im gettin' ready!" ); ChatBox.AddChatEntry( To.Everyone, Shooter.Name, "Im gettin' ready!" );
Taunted++; Taunted++;
} }
return false;
} }
public override void Cleanup() public override void Cleanup()

View File

@@ -5,10 +5,13 @@
@attribute [StyleSheet] @attribute [StyleSheet]
@inherits Panel @inherits Panel
@if (ShouldShowCursor) <root>
{ @if (ShouldShowCursor)
<root/> {
} <div class="camera-cursor"/>
}
</root>
@code { @code {

View File

@@ -1,3 +1,5 @@
CameraCursor { CameraCursor {
pointer-events: all; .camera-cursor {
pointer-events: all;
}
} }

View File

@@ -10,9 +10,9 @@
<root> <root>
<div class="scoreboard-panel"> <div class="scoreboard-panel">
@foreach (var player in Luckers) @foreach (var lucker in Luckers)
{ {
<label>@player.Name</label> <label>@lucker.Name</label>
} }
</div> </div>
</root> </root>
@@ -23,7 +23,7 @@
protected override int BuildHash() protected override int BuildHash()
{ {
return HashCode.Combine(Luckers.Select(player => player.Name).ToList()); return HashCode.Combine(Luckers.Select(lucker => lucker.Name).ToList());
} }
} }

View File

@@ -0,0 +1,121 @@
@using Sandbox;
@using System;
@using System.Linq;
@using System.Threading.Tasks;
@using Sandbox.Menu;
@using Sandbox.UI;
@namespace LuckerGame.UI.MainMenu
@inherits Panel
<root>
<label class="game-title">
@Game.Menu.Package.Title
</label>
@if ( Lobby == null )
{
<div class="controls">
<a class="button">Loading...</a>
<a class="button" href="/lobby/list">Return</a>
</div>
}
else
{
<div class="controls">
<div class="col">
<label>Members (@Lobby.MemberCount/@Lobby.MaxMembers)</label>
<div class="span">
@foreach (var member in Lobby.Members)
{
<img class="avatar" src="avatar:@member.Id" tooltip="@member.Name" />
}
</div>
</div>
@if ( Lobby.Owner.IsMe )
{
<div class="span">
@if ( MaxPlayersSupported > 1 )
{
<FormGroup class="form-group">
<Label>Maximum Players</Label>
<Control>
<SliderControl ShowRange=@true Min=@(1f) Max=@MaxPlayersSupported Value:bind=@Game.Menu.Lobby.MaxMembers />
</Control>
</FormGroup>
}
<FormGroup class="form-group">
<Label>Map</Label>
<Control>
<SlimPackageCard OnLaunch=@OnMapClicked Package=@MapPackage />
</Control>
</FormGroup>
</div>
}
<div class="spacer" />
<a class="button" @onclick=@LeaveLobby>Leave Lobby</a>
<a class="button" @onclick=@Start>Start</a>
<a class="button" href="/lobby/list">Return</a>
</div>
}
</root>
@code
{
Friend Owner => Lobby.Owner;
ILobby Lobby => Game.Menu.Lobby;
int MaxPlayersSupported { get; set; } = 1;
Package MapPackage { get; set; }
void OnMapClicked()
{
Game.Overlay.ShowPackageSelector( "type:map sort:popular", OnMapSelected );
StateHasChanged();
}
void OnMapSelected( Package map )
{
MapPackage = map;
Game.Menu.Lobby.Map = map.FullIdent;
StateHasChanged();
}
public void LeaveLobby()
{
Lobby?.Leave();
this.Navigate( "/lobby/list" );
}
async Task Start()
{
await Game.Menu.StartServerAsync( Game.Menu.Lobby.MaxMembers, $"{Game.Menu.Lobby.Owner.Name}'s game", Game.Menu.Lobby.Map );
}
async void FetchPackage()
{
MapPackage = await Package.FetchAsync( Game.Menu.Lobby?.Map ?? "facepunch.square", true );
}
protected override void OnAfterTreeRender( bool firstTime )
{
FetchPackage();
}
protected override void OnParametersSet()
{
MaxPlayersSupported = Game.Menu.Package.GetMeta<int>( "MaxPlayers", 1 );
}
}

View File

@@ -0,0 +1,40 @@
@using Sandbox;
@using System.Linq;
@using System.Threading.Tasks;
@using Sandbox.Menu;
@using Sandbox.UI;
@namespace LuckerGame.UI.MainMenu
<root>
<div class="game-title">
@Game.Menu.Package.Title
</div>
<div class="controls">
@if (Game.InGame)
{
<a class="button" onclick=@LeaveGame>Leave</a>
}
else
{
<a class="button" href="/setup">Play</a>
<a class="button" href="/lobby/list">Lobbies</a>
@if ( Game.Menu.Package.SupportsSavedGames && Game.Menu.SavedGames.Any())
{
<a class="button" href="/setup/save">Load Save</a>
}
}
<a class="button" @onclick=@Game.Menu.Close>Quit</a>
</div>
</root>
@code
{
void LeaveGame()
{
Game.Menu.LeaveServer( "Leaving" );
}
}

View File

@@ -0,0 +1,31 @@
@using Sandbox;
@using Sandbox.UI;
@using System.Linq;
@using System.Threading.Tasks;
@using Sandbox.Menu;
@inherits RootPanel
@implements Sandbox.Menu.ILoadingScreenPanel
@attribute [StyleSheet]
@namespace LuckerGame.UI.MainMenu
<root style="flex-direction: column;">
<div class="background" />
<div style="flex-grow: 1;" />
<div class="controls" style="flex-direction: row; justify-content: center;">
<a class="button">@( Progress.Title ?? "Loading..." )</a>
</div>
</root>
@code
{
public LoadingProgress Progress;
public void OnLoadingProgress( LoadingProgress progress )
{
Progress = progress;
StateHasChanged();
}
}

View File

@@ -0,0 +1,50 @@
LoadingScreen
{
background-color: #262934;
padding: 128px 128px;
opacity: 1;
flex-direction: column;
font-size: 25px;
width: 100%;
height: 100%;
position: absolute;
transition: all 0.3s ease-out;
color: rgba( white, 0.8 );
.background
{
position: absolute;
width: 100%;
height: 100%;
background-image: url( https://files.facepunch.com/tony/1b1311b1/boxes.webm );
opacity: 0.2;
background-size: contain;
filter: blur( 20px );
mask: linear-gradient( 45deg, white, white, black );
mask-scope: filter;
}
&:intro
{
opacity: 0;
transform: scaleX( 1.1 );
}
.game-title
{
font-family: Roboto;
font-weight: 700;
font-size: 70px;
color: rgba( white, 1 );
}
.controls
{
flex-direction: column;
gap: 50px;
align-items: flex-start;
font-family: Roboto;
text-transform: uppercase;
}
}

View File

@@ -0,0 +1,66 @@
@using Sandbox;
@using System;
@using System.Linq;
@using System.Threading.Tasks;
@using Sandbox.Menu;
@using Sandbox.UI;
@namespace LuckerGame.UI.MainMenu
@inherits Panel
<root>
<label class="game-title">
@Game.Menu.Package.Title
</label>
<div class="controls">
<div class="span">
<label>Showing @Game.Menu.Lobbies.Count() @(Game.Menu.Lobbies.Count() == 1 ? "lobby" : "lobbies")</label>
<i class="with-click" tooltip="Refresh lobbies" @onclick=@Refresh>refresh</i>
</div>
<div class="scroll">
@foreach ( var lobby in Game.Menu.Lobbies )
{
<a class="button" @onclick=@( () => JoinLobby( lobby ) ) >@(lobby.Owner.Name)'s lobby (@lobby.MemberCount/@lobby.MaxMembers)</a>
}
</div>
<div class="spacer" />
<a class="button" @onclick=@CreateLobbyAsync>Create Lobby</a>
<a class="button" href="/">Return</a>
</div>
</root>
@code
{
public async void CreateLobbyAsync()
{
await Game.Menu.CreateLobbyAsync( 64, "game", true );
Game.Menu.Lobby.Map = "facepunch.square";
this.Navigate( "/lobby/active" );
}
public async void JoinLobby( ILobby lobby )
{
if ( lobby == null ) return;
// don't exist in two lobbies at once
Game.Menu.Lobby?.Leave();
await lobby.JoinAsync();
this.Navigate( "/lobby/active" );
}
public async void Refresh()
{
await Game.Menu.QueryLobbiesAsync( null, 1 );
StateHasChanged();
}
protected override int BuildHash()
{
return HashCode.Combine( Game.Menu.Lobbies.Count() );
}
}

View File

@@ -0,0 +1,40 @@
@using System;
@using Sandbox;
@using Sandbox.UI;
@inherits Sandbox.UI.NavHostPanel
@implements Sandbox.Menu.IGameMenuPanel
@attribute [StyleSheet]
@namespace LuckerGame.UI.MainMenu
<root style="flex-direction: column;">
<div class="background" />
<div class="navigator-canvas" slot="navigator-canvas" />
</root>
@code
{
public MainMenu()
{
DefaultUrl = "/";
AddDestination( "/", typeof( FrontPage ) );
AddDestination( "/setup", typeof( SetupGame ) );
AddDestination( "/lobby/list", typeof( LobbyBrowser ) );
AddDestination( "/lobby/active", typeof( ActiveLobby ) );
BindClass( "ingame", () => Game.InGame );
}
[GameEvent.Menu.ServerJoined]
public void OnServerJoined() => Navigate( "/" );
[GameEvent.Menu.ServerLeave]
public void OnServerLeave() => Navigate ("/" );
protected override int BuildHash()
{
return HashCode.Combine( Game.InGame, Game.Menu.Lobby, Game.Menu.Lobby?.Map );
}
}

View File

@@ -0,0 +1,195 @@
MainMenu
{
background-color: #262934;
padding: 128px 128px;
opacity: 1;
flex-direction: column;
font-size: 25px;
width: 100%;
height: 100%;
position: absolute;
transition: all 0.3s ease-out;
color: rgba( white, 0.8 );
.background
{
position: absolute;
width: 100%;
height: 100%;
background-image: url( https://files.facepunch.com/tony/1b1311b1/boxes.webm );
opacity: 0.2;
background-size: cover;
filter: blur( 20px );
mask: linear-gradient( 45deg, white, white, black );
mask-scope: filter;
background-repeat: no-repeat;
}
&:intro
{
opacity: 0;
transform: scaleX( 1.1 );
}
&.ingame
{
background-color: #262934ee;
backdrop-filter: blur(10px);
.background
{
opacity: 0;
}
}
.scroll
{
flex-direction: column;
max-height: 386px;
overflow: scroll;
gap: 50px;
}
.spacer
{
height: 1px;
background-image: linear-gradient( to right, rgba( white, 0.4 ), rgba( white, 0 ) );
width: 512px;
margin: 16px 0px;
}
.game-title
{
font-family: Roboto Condensed;
font-weight: 700;
font-size: 70px;
color: rgba( white, 1 );
padding-bottom: 64px;
}
.col
{
flex-direction: column;
gap: 16px;
}
.controls
{
flex-direction: column;
gap: 50px;
align-items: flex-start;
text-transform: uppercase;
a, .button
{
font-family: Roboto;
&:hover
{
color: rgba( white, 1 );
font-weight: 900;
sound-in: ui.button.over;
cursor: pointer;
}
&:active
{
sound-in: ui.button.press;
}
}
.span
{
gap: 128px;
}
}
.navigator-canvas
{
flex-direction: column;
height: 100%;
flex-grow: 1;
flex-shrink: 0;
}
.navigator-body
{
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
flex-direction: column;
justify-content: center;
transition: opacity 0.15s ease-out;
&.hidden
{
opacity: 0;
}
}
}
FormGroup
{
flex-direction: column;
min-width: 300px;
gap:16px;
}
SlimPackageCard
{
flex-direction: row;
gap: 12px;
align-items: center;
> i
{
&:hover
{
cursor: pointer;
color: white;
transform: scale( 1.1 );
sound-in: ui.button.over;
}
&:active
{
sound-in: ui.button.press;
}
}
}
i
{
font-family: Material Icons;
text-transform: lowercase;
&.with-click
{
&:hover
{
cursor: pointer;
color: white;
transform: scale( 1.1 );
sound-in: ui.button.over;
}
&:active
{
sound-in: ui.button.press;
}
}
}
.avatar
{
width: 64px;
height: 64px;
&:hover
{
border: 1px solid white;
}
}

View File

@@ -0,0 +1,44 @@
@using Sandbox;
@using System;
@using System.Linq;
@using Sandbox.UI;
@namespace LuckerGame.UI.MainMenu
@inherits Panel
<root>
<label class="game-title">
@Game.Menu.Package.Title
</label>
<div class="controls">
@foreach ( var save in Game.Menu.SavedGames.OrderByDescending( x => x.Time ) )
{
<a class="button" @onclick=@(() => LoadSavedGame( save ))>@save.Name - @save.Time</a>
}
<div class="spacer" />
<a class="button" href="/">Return</a>
</div>
</root>
@code
{
async void LoadSavedGame( SavedGame save )
{
if ( save != null )
{
Game.Menu.Lobby.SavedGame = save.Name;
if ( !string.IsNullOrEmpty( save.Map ) )
Game.Menu.Lobby.Map = save.Map;
await Game.Menu.StartServerAsync( Game.Menu.Lobby.MaxMembers, Game.Menu.Lobby.Title, Game.Menu.Lobby.Map ?? "facepunch.square" );
}
}
protected override int BuildHash()
{
return HashCode.Combine( Game.Menu.Lobby, Game.Menu.Lobby?.Map );
}
}

View File

@@ -0,0 +1,78 @@
@using Sandbox;
@using System;
@using System.Linq;
@using System.Threading.Tasks;
@using Sandbox.Menu;
@using Sandbox.UI;
@namespace LuckerGame.UI.MainMenu
@inherits Panel
<root>
<label class="game-title">
@Game.Menu.Package.Title
</label>
<div class="controls">
<div class="span">
@if ( MaxPlayersSupported > 1 )
{
<FormGroup class="form-group">
<Label>Maximum Players</Label>
<Control>
<SliderControl ShowRange=@true Min=@(1f) Max=@MaxPlayersSupported Value:bind=@Game.Menu.Lobby.MaxMembers />
</Control>
</FormGroup>
}
<FormGroup class="form-group">
<Label>Map</Label>
<Control>
<SlimPackageCard OnLaunch=@OnMapClicked Package=@MapPackage />
</Control>
</FormGroup>
</div>
<div class="spacer" />
<a class="button" onclick=@Play>Start</a>
<a class="button" href="/">Return</a>
</div>
</root>
@code
{
int MaxPlayersSupported { get; set; } = 1;
int MaxPlayers { get; set; } = 1;
Package MapPackage { get; set; }
void OnMapClicked()
{
Game.Overlay.ShowPackageSelector( "type:map sort:popular", OnMapSelected );
StateHasChanged();
}
void OnMapSelected( Package map )
{
MapPackage = map;
StateHasChanged();
}
protected override async Task OnParametersSetAsync()
{
MaxPlayersSupported = Game.Menu.Package.GetMeta<int>( "MaxPlayers", 1 );
MaxPlayers = MaxPlayersSupported;
MapPackage = await Package.FetchAsync( "facepunch.square", false );
StateHasChanged();
}
async Task Play()
{
await Game.Menu.StartServerAsync( MaxPlayers, $"My game", MapPackage.FullIdent );
}
protected override int BuildHash()
{
return HashCode.Combine( MaxPlayers, MapPackage );
}
}

View File

@@ -0,0 +1,32 @@
@using System;
@using Sandbox;
@namespace LuckerGame.UI.MainMenu
@inherits Sandbox.UI.Panel
<root>
@if ( Package == null )
{
<div class="button" @onclick=@OnCardClicked>Select Package</div>
}
else
{
<div class="button" @onclick=@OnCardClicked>@Package.Title</div>
<i tooltip="See information about this package" @onclick=@( () => Game.Overlay.ShowPackageModal( Package.FullIdent ) )>info</i>
}
</root>
@code
{
public Package Package { get; set; }
public System.Action OnLaunch { get; set; }
void OnCardClicked()
{
OnLaunch?.Invoke();
}
protected override int BuildHash()
{
return HashCode.Combine( Package );
}
}

View File

@@ -27,7 +27,7 @@
<div class="voting-panel primary-color-translucent-background"> <div class="voting-panel primary-color-translucent-background">
@if (RoundManager.RoundState == RoundState.NotStarted) @if (RoundManager.RoundState == RoundState.NotStarted)
{ {
<label class="header">Waiting for players...</label> <label class="header">Waiting for luckers...</label>
} }
else else
{ {