6 Commits

15 changed files with 196 additions and 103 deletions

View File

@@ -46,7 +46,7 @@
"Choices": [],
"ConVarName": "lucker_minigames_per_round",
"DisplayName": "Minigames Per Round",
"DefaultValue": "0",
"DefaultValue": "1",
"Description": "The number of minigames played per round",
"Group": "Other",
"Minimum": 1,

View File

@@ -1,22 +1,39 @@
using LuckerGame.Components.Lucker.Cameras;
using LuckerGame.EntityComponents.Lucker;
using LuckerGame.Events;
using Sandbox;
namespace LuckerGame.Entities;
/// <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
/// </summary>
public partial class Lucker : Entity
{
/// <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>
public Entity Pawn { get; set; }
[Net] private Entity InternalPawn { get; set; }
/// <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>
[Net] public bool Ready { get; set; }
@@ -24,21 +41,24 @@ public partial class Lucker : Entity
/// This Lucker's camera
/// </summary>
[BindComponent] public AbstractCamera Camera { get; }
[BindComponent] public LuckerStats Stats { get; }
/// <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>
/// <param name="client">the client to own the player</param>
/// <returns>the newly created player</returns>
/// <param name="client">the client to own the lucker</param>
/// <returns>the newly created lucker</returns>
public static Lucker CreateLuckerForClient( IClient client )
{
var player = new Lucker();
client.Pawn = player;
player.Owner = client as Entity;
player.Name = client.Name;
var camera = player.Components.Create<RTSCamera>();
var lucker = new Lucker();
client.Pawn = lucker;
lucker.Owner = client as Entity;
lucker.Name = client.Name;
lucker.Components.Create<RTSCamera>();
lucker.Components.Create<LuckerStats>();
return player;
return lucker;
}
/// <summary>
@@ -49,12 +69,12 @@ public partial class Lucker : Entity
public static void ReadyUpCommand(bool readyState)
{
var client = ConsoleSystem.Caller;
var player = client.Pawn as Lucker;
player.SetReady( readyState );
var lucker = client.Pawn as Lucker;
lucker.SetReady( readyState );
}
/// <summary>
/// Sets this player's ready state
/// Sets this lucker's ready state
/// </summary>
/// <param name="ready">the ready state being set</param>
public void SetReady(bool ready)
@@ -62,7 +82,7 @@ public partial class Lucker : Entity
Ready = ready;
if ( Game.IsServer )
{
Event.Run( LuckerEvent.PlayerReady, this, ready );
Event.Run( LuckerEvent.LuckerReady, this, ready );
}
}

View File

@@ -12,9 +12,20 @@ namespace LuckerGame.Entities;
/// </summary>
public partial class MinigameManager : Entity
{
/// <summary>
/// The currently loaded minigame
/// </summary>
[Net] public Minigame LoadedMinigame { get; private set; }
/// <summary>
/// A cached list of available minigames. Gets reloaded on a hotreload
/// </summary>
private List<TypeDescription> AvailableMinigames { get; set; }
private List<Lucker> InvolvedPlayers { get; set; }
/// <summary>
/// The luckers involved in the current minigame
/// </summary>
private List<Lucker> InvolvedLuckers { get; set; }
public override void Spawn()
{
@@ -22,14 +33,17 @@ public partial class MinigameManager : Entity
FindMinigames();
}
public void StartMinigame(List<Lucker> players, string minigameName = null)
public void StartMinigame(List<Lucker> luckers, string minigameName = null)
{
InvolvedPlayers = players.ToList();
InvolvedLuckers = luckers.ToList();
if (CheckForMinigames())
{
LoadedMinigame = string.IsNullOrEmpty( minigameName ) ? TypeLibrary.Create<Minigame>(AvailableMinigames.OrderBy( _ => Guid.NewGuid() ).FirstOrDefault().TargetType) : 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}" );
LoadedMinigame.Initialize( players );
LoadedMinigame.Initialize( luckers );
}
}
@@ -58,19 +72,19 @@ public partial class MinigameManager : Entity
}
/// <summary>
/// Goes through the players included in the loaded minigame and deletes and nulls out any pawns assigned to them
/// Goes through the luckers included in the loaded minigame and deletes and nulls out any pawns assigned to them
/// </summary>
private void CleanupPlayerPawns()
private void CleanupLuckerPawns()
{
if ( LoadedMinigame is not { IsValid: true } || InvolvedPlayers == null)
if ( LoadedMinigame is not { IsValid: true } || InvolvedLuckers == null)
{
Log.Warning( "Attempted to clean up players without a minigame loaded!" );
return;
}
InvolvedPlayers.ForEach( player =>
InvolvedLuckers.ForEach( lucker =>
{
player.Pawn.Delete();
player.Pawn = null;
lucker.Pawn?.Delete();
lucker.Pawn = null;
} );
}
@@ -89,10 +103,16 @@ public partial class MinigameManager : Entity
{
return false;
}
LoadedMinigame.Cleanup();
LoadedMinigame = null;
EndMinigame();
return true;
}
private void EndMinigame()
{
LoadedMinigame.Cleanup();
CleanupLuckerPawns();
LoadedMinigame.Delete();
LoadedMinigame = null;
InvolvedLuckers = null;
}
}

View File

@@ -47,12 +47,21 @@ public partial class RoundManager : Entity
#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")]
public static int MinigamesLeftInRound { get; set; }
private static int MinigamesPerRound { get; set; }
private List<Lucker> Players { get; set; }
/// <summary>
/// The number of minigames left in the current round
/// </summary>
public int MinigamesLeftInRound { get; set; }
/// <summary>
/// The luckers playing in the current round
/// </summary>
private List<Lucker> Luckers { get; set; }
#endregion
/// <inheritdoc/>
@@ -83,7 +92,7 @@ public partial class RoundManager : Entity
MinigamesLeftInRound--;
if ( MinigamesLeftInRound > 0 )
{
MinigameManager.StartMinigame( Players );
MinigameManager.StartMinigame( Luckers );
}
else
{
@@ -94,11 +103,12 @@ public partial class RoundManager : Entity
}
/// <summary>
/// Is triggered whenever a player readies up
/// Is triggered whenever a lucker readies up or readies down
/// </summary>
/// <param name="readyLucker">the player that readied up, discarded</param>
[LuckerEvent.PlayerReady]
public void HandlePlayerReady( Lucker readyLucker, bool ready )
/// <param name="readyLucker">the lucker that readied up</param>
/// <param name="ready">the lucker's ready state</param>
[LuckerEvent.LuckerReady]
public void HandleLuckerReady( Lucker readyLucker, bool ready )
{
if ( RoundState != RoundState.NotStarted && RoundState != RoundState.StartCountdown )
{
@@ -107,9 +117,9 @@ public partial class RoundManager : Entity
Log.Info( $"{readyLucker.Client.Name} set ready to {ready}" );
var message = $"{readyLucker.Client.Name} is {(ready ? "now ready." : "no longer ready.")}";
ChatBox.AddInformation( To.Everyone, message );
var players = All.OfType<Lucker>().ToList();
var readiedCount = players.Count( player => player.Ready );
var totalCount = players.Count;
var luckers = All.OfType<Lucker>().ToList();
var readiedCount = luckers.Count( lucker => lucker.Ready );
var totalCount = luckers.Count;
if ( (float)readiedCount / totalCount > RequiredReadyPercent && RoundState == RoundState.NotStarted )
{
Log.Info( "Countdown started" );
@@ -132,13 +142,13 @@ public partial class RoundManager : Entity
}
RoundState = RoundState.InProgress;
Players = All.OfType<Lucker>().ToList();
Players.ForEach( player =>
Luckers = All.OfType<Lucker>().ToList();
Luckers.ForEach( lucker =>
{
player.Ready = false;
lucker.Ready = false;
} );
MinigamesLeftInRound = MinigamesPerRound;
MinigameManager.StartMinigame( Players, minigameName );
MinigameManager.StartMinigame( Luckers, minigameName );
}
[ConCmd.Server( "start_round" )]

View File

@@ -76,7 +76,7 @@ public partial class Weapon : AnimatedEntity
}
/// <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>
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 const string PlayerReady = "lucker.playerReady";
public const string LuckerReady = "lucker.luckerReady";
/// <summary>
/// Event is run on the server whenever a player changes ready state
/// The event handler is given the player that readied up and their new ready state
/// Event is run on the server whenever a lucker changes ready state
/// The event handler is given the lucker that readied up and their new ready state
/// </summary>
[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,8 +15,8 @@ public abstract class Minigame : Entity
/// <summary>
/// Initializes the minigame with a list of luckers playing it.
/// </summary>
/// <param name="players">the players who made it into the minigame</param>
public abstract void Initialize(List<Lucker> players);
/// <param name="luckers">the luckers who made it into the minigame</param>
public abstract void Initialize(List<Lucker> luckers);
/// <summary>
/// Once a minigame is loaded and initialized, this method is called once per server tick.

View File

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

View File

@@ -14,50 +14,53 @@ namespace LuckerGame.Minigames.RussianRoulette;
public class RussianRouletteMinigame : Minigame
{
public override string Name => "Russian Roulette";
private List<Lucker> Players { get; set; }
private List<Lucker> Luckers { get; set; }
private Pawn Shooter { get; set; }
private const float ShooterDistance = 80f;
private const float TimeBetweenShots = 7f;
private const float TimeBetweenDeathAndEnd = 5f;
private const string ShooterName = "The Russian";
private int Taunted = 0;
private Pawn ShooterTarget;
private List<Pawn> DeadVictims => Players
.Select( player => player.Pawn as Pawn )
.Where( pawn => !pawn.IsValid || pawn.LifeState != LifeState.Alive )
private List<Pawn> DeadVictims => Luckers
.Select( lucker => lucker.Pawn as Pawn )
.Where( pawn => pawn is not { IsValid: true } || pawn.LifeState != LifeState.Alive )
.ToList();
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.Name = ShooterName;
var shooterInventory = Shooter.Components.Create<PawnInventory>();
shooterInventory.AddWeapon( new RussianPistol() );
// Setup cameras for players
Players.ForEach( player =>
// Setup cameras for luckers
Luckers.ForEach( lucker =>
{
player.Components.Create<RTSCamera>();
player.Position = Shooter.Position;
lucker.Components.Create<RTSCamera>();
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 pawn = new Pawn();
pawn.Name = player.Name;
pawn.Name = lucker.Name;
pawn.Tags.Add( "victim" );
pawn.Health = 1;
player.Pawn = pawn;
pawn.DressFromClient( player.Client );
lucker.Pawn = pawn;
pawn.DressFromClient( lucker.Client );
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);
} );
TimeSinceShot = 0;
@@ -66,11 +69,12 @@ public class RussianRouletteMinigame : Minigame
public override bool Tick()
{
// Someone is dead, we're getting ready to end
if ( DeadVictims.Any() )
{
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;
TimeSinceDeadVictim = 0;
}
@@ -86,21 +90,22 @@ public class RussianRouletteMinigame : Minigame
Shooter.Inventory.ActiveWeapon.PrimaryAttack();
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)
{
var victim = Players.Select( player => player.Pawn as Pawn )
ShooterTarget = Luckers.Select( lucker => lucker.Pawn as Pawn )
.OrderBy( _ => Guid.NewGuid() )
.FirstOrDefault();
Shooter.LookAt( victim.Position );
ChatBox.AddChatEntry( To.Everyone, "Shooter", $"I'm gonna eat you up, {victim.Name}" );
Shooter.LookAt( ShooterTarget.Position );
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++;
}
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++;
}

View File

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

View File

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

View File

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

View File

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