From 285a130a813a65b1adb8a11ee51f1368aefbce3e Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 2 Aug 2023 19:47:53 -0400 Subject: [PATCH] Initial commit --- .editorconfig | 100 +++++++ .gitignore | 18 ++ .sbproj | 44 +++ README.md | 11 + code/Entities/Lucker.cs | 87 ++++++ code/Entities/MinigameManager.cs | 49 ++++ code/Entities/Pawn.cs | 133 ++++++++++ code/Entities/RoundManager.cs | 120 +++++++++ code/Entities/Weapons/Weapon.cs | 250 ++++++++++++++++++ code/Entities/Weapons/WeaponViewModel.cs | 22 ++ .../Client/ClientComponent.cs | 11 + .../Lucker/Cameras/AbstractCamera.cs | 36 +++ .../Lucker/Cameras/RTSCamera.cs | 47 ++++ .../Lucker/Cameras/TopDownCamera.cs | 97 +++++++ .../Lucker/LuckerClientInput.cs | 15 ++ code/EntityComponents/Pawn/PawnAnimator.cs | 21 ++ .../Pawn/UserPawnController.cs | 174 ++++++++++++ code/Enums/RoundState.cs | 20 ++ code/Events/MinigameEndEvent.cs | 18 ++ code/Events/PlayerReadyEvent.cs | 20 ++ code/Game.cs | 69 +++++ code/Minigames/Minigame.cs | 22 ++ .../RussianRouletteMinigame.cs | 36 +++ code/UI/Hud.razor | 27 ++ code/UI/Hud.razor.scss | 30 +++ code/UI/HudComponents/Scoreboard.razor | 29 ++ code/UI/HudComponents/Scoreboard.razor.scss | 3 + code/UI/MenuComponents/LuckerButton.razor | 26 ++ .../UI/MenuComponents/LuckerButton.razor.scss | 15 ++ code/UI/MenuComponents/LuckerSpinner.razor | 9 + .../MenuComponents/LuckerSpinner.razor.scss | 20 ++ code/UI/Menus/VotingLobby.razor | 77 ++++++ code/UI/Menus/VotingLobby.razor.scss | 43 +++ 33 files changed, 1699 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .sbproj create mode 100644 README.md create mode 100644 code/Entities/Lucker.cs create mode 100644 code/Entities/MinigameManager.cs create mode 100644 code/Entities/Pawn.cs create mode 100644 code/Entities/RoundManager.cs create mode 100644 code/Entities/Weapons/Weapon.cs create mode 100644 code/Entities/Weapons/WeaponViewModel.cs create mode 100644 code/EntityComponents/Client/ClientComponent.cs create mode 100644 code/EntityComponents/Lucker/Cameras/AbstractCamera.cs create mode 100644 code/EntityComponents/Lucker/Cameras/RTSCamera.cs create mode 100644 code/EntityComponents/Lucker/Cameras/TopDownCamera.cs create mode 100644 code/EntityComponents/Lucker/LuckerClientInput.cs create mode 100644 code/EntityComponents/Pawn/PawnAnimator.cs create mode 100644 code/EntityComponents/Pawn/UserPawnController.cs create mode 100644 code/Enums/RoundState.cs create mode 100644 code/Events/MinigameEndEvent.cs create mode 100644 code/Events/PlayerReadyEvent.cs create mode 100644 code/Game.cs create mode 100644 code/Minigames/Minigame.cs create mode 100644 code/Minigames/RussianRoulette/RussianRouletteMinigame.cs create mode 100644 code/UI/Hud.razor create mode 100644 code/UI/Hud.razor.scss create mode 100644 code/UI/HudComponents/Scoreboard.razor create mode 100644 code/UI/HudComponents/Scoreboard.razor.scss create mode 100644 code/UI/MenuComponents/LuckerButton.razor create mode 100644 code/UI/MenuComponents/LuckerButton.razor.scss create mode 100644 code/UI/MenuComponents/LuckerSpinner.razor create mode 100644 code/UI/MenuComponents/LuckerSpinner.razor.scss create mode 100644 code/UI/Menus/VotingLobby.razor create mode 100644 code/UI/Menus/VotingLobby.razor.scss diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fb22870 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,100 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] +indent_style = tab +indent_size = tab +tab_size = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = true + + +#### C# Coding Conventions #### + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Code-block preferences +csharp_prefer_braces = true:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = no_change +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = true +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = true +csharp_space_between_parentheses = control_flow_statements +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34ee852 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ + +# This file describes files and paths that should not be tracked by Git version control +# https://git-scm.com/docs/gitignore + +# Auto-generated code editor files +.vs/* +.vscode/* +*.csproj +obj/* +Properties/* +code/obj/* +code/Properties/* + +# Auto-generated asset related files +*.generated.* +*.*_c +*.los +*.vpk \ No newline at end of file diff --git a/.sbproj b/.sbproj new file mode 100644 index 0000000..b732cf4 --- /dev/null +++ b/.sbproj @@ -0,0 +1,44 @@ +{ + "Title": "Are You \u0026 Lucker", + "Type": "game", + "Org": "local", + "Ident": "are_you___lucker", + "Tags": null, + "Schema": 1, + "HasAssets": true, + "AssetsPath": "", + "Resources": null, + "MenuResources": null, + "HasCode": true, + "CodePath": "/code/", + "PackageReferences": [], + "EditorReferences": null, + "Metadata": { + "MaxPlayers": 8, + "MinPlayers": 1, + "GameNetworkType": "Multiplayer", + "MapSelect": "Unrestricted", + "MapList": [ + "facepunch.square" + ], + "RankType": "None", + "PerMapRanking": false, + "LeaderboardType": "None", + "GameCategory": "TechDemos", + "ProjectTemplate": null, + "TickRate": 50, + "CsProjName": "", + "LaunchConfigs": [ + { + "Name": "My New Config", + "GameIdent": "local.are_you___lucker#local", + "MapName": "voximity.flat_plane", + "MaxPlayers": 1, + "GameSettings": {}, + "Addons": "", + "PreLaunchCommand": "", + "PostLaunchCommand": "" + } + ] + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba08dda --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Top Down Template +Designed as a minimal starting point for your projects in s&box. + +## Features +- Top down camera +- Simple pawn controller + +## Controls +- Regular movement with WASD +- Rotate the camera with right mouse button +- Zoom the camera with mouse wheel diff --git a/code/Entities/Lucker.cs b/code/Entities/Lucker.cs new file mode 100644 index 0000000..3394cd4 --- /dev/null +++ b/code/Entities/Lucker.cs @@ -0,0 +1,87 @@ +using LuckerGame.Components.Lucker.Cameras; +using LuckerGame.Events; +using Sandbox; + +namespace LuckerGame.Entities; + +/// +/// Represents a Player. +/// This could belong to a Client or a Bot and represents a common entity to operate on for games and keeping score +/// +public partial class Lucker : Entity +{ + /// + /// The entity this Player currently controls + /// + public Entity Pawn { get; set; } + + /// + /// Before the round has started, this player indicated they were ready + /// + [Net] public bool Ready { get; set; } + + /// + /// This Lucker's camera + /// + [BindComponent] public AbstractCamera Camera { get; } + + /// + /// Creates and properly sets up a Player entity for a given client + /// + /// the client to own the player + /// the newly created player + 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(); + + return player; + } + + /// + /// Allows clients to request setting their ready state + /// + /// whether they are readying up or calming down + [ConCmd.Server("set_ready")] + public static void ReadyUpCommand(bool readyState) + { + var client = ConsoleSystem.Caller; + var player = client.Pawn as Lucker; + player.SetReady( readyState ); + } + + /// + /// Sets this player's ready state + /// + /// the ready state being set + public void SetReady(bool ready) + { + Ready = ready; + if ( Game.IsServer ) + { + Event.Run( LuckerEvent.PlayerReady, this, ready ); + } + } + + /// + public override void Simulate( IClient cl ) + { + base.Simulate( cl ); + + } + + public override void FrameSimulate( IClient cl ) + { + base.FrameSimulate( cl ); + Camera?.Update(); + } + + public override void BuildInput() + { + base.BuildInput(); + Camera?.BuildInput(); + } +} diff --git a/code/Entities/MinigameManager.cs b/code/Entities/MinigameManager.cs new file mode 100644 index 0000000..f7a9be4 --- /dev/null +++ b/code/Entities/MinigameManager.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LuckerGame.Minigames; +using Sandbox; +using Sandbox.UI; + +namespace LuckerGame.Entities; + +/// +/// Manages minigames +/// +public partial class MinigameManager : Entity +{ + [Net] public Minigame LoadedMinigame { get; private set; } + private List AvailableMinigames { get; set; } + + public override void Spawn() + { + base.Spawn(); + FindMinigames(); + } + + public void StartRandomMinigame(List players) + { + if ( (AvailableMinigames?.Count ?? 0) == 0 ) + { + Log.Error( "Attempted to start minigame, but none available" ); + return; + } + + LoadedMinigame = AvailableMinigames.OrderBy( _ => Guid.NewGuid() ).FirstOrDefault(); + ChatBox.AddInformation( To.Everyone, $"Starting {LoadedMinigame.Name}" ); + LoadedMinigame.Initialize( players ); + } + + private void FindMinigames() + { + AvailableMinigames = TypeLibrary.GetTypes() + .Where( type => !type.IsAbstract && !type.IsInterface ) + .Select( td => TypeLibrary.Create( td.TargetType ) ).ToList(); + } + + [Event.Hotload] + private void Reload() + { + FindMinigames(); + } +} diff --git a/code/Entities/Pawn.cs b/code/Entities/Pawn.cs new file mode 100644 index 0000000..c835b48 --- /dev/null +++ b/code/Entities/Pawn.cs @@ -0,0 +1,133 @@ +using System.ComponentModel; +using LuckerGame.Components.Pawn; +using Sandbox; + +namespace LuckerGame.Entities; + +/// +/// Represents an entity in the world. Could be controlled by a Lucker or a minigame +/// +public partial class Pawn : AnimatedEntity +{ + [ClientInput] + public Vector3 InputDirection { get; set; } + + [ClientInput] + public Angles ViewAngles { get; set; } + + /// + /// Position a player should be looking from in world space. + /// + [Browsable( false )] + public Vector3 EyePosition + { + get => Transform.PointToWorld( EyeLocalPosition ); + set => EyeLocalPosition = Transform.PointToLocal( value ); + } + + /// + /// Position a player should be looking from in local to the entity coordinates. + /// + [Net, Predicted, Browsable( false )] + public Vector3 EyeLocalPosition { get; set; } + + /// + /// Rotation of the entity's "eyes", i.e. rotation for the camera when this entity is used as the view entity. + /// + [Browsable( false )] + public Rotation EyeRotation + { + get => Transform.RotationToWorld( EyeLocalRotation ); + set => EyeLocalRotation = Transform.RotationToLocal( value ); + } + + /// + /// Rotation of the entity's "eyes", i.e. rotation for the camera when this entity is used as the view entity. In local to the entity coordinates. + /// + [Net, Predicted, Browsable( false )] + public Rotation EyeLocalRotation { get; set; } + + public BBox Hull + { + get => new + ( + new Vector3( -16, -16, 0 ), + new Vector3( 16, 16, 64 ) + ); + } + + [BindComponent] public UserPawnController Controller { get; } + [BindComponent] public PawnAnimator Animator { get; } + + public override Ray AimRay => new Ray( EyePosition, EyeRotation.Forward ); + + /// + /// Called when the entity is first created + /// + public override void Spawn() + { + SetModel( "models/citizen/citizen.vmdl" ); + + EnableDrawing = true; + EnableHideInFirstPerson = true; + EnableShadowInFirstPerson = true; + } + + public void Respawn() + { + Components.Create(); + Components.Create(); + } + + public void DressFromClient( IClient cl ) + { + var c = new ClothingContainer(); + c.LoadFromClient( cl ); + c.DressEntity( this ); + } + + public override void Simulate( IClient cl ) + { + SimulateRotation(); + Controller?.Simulate( cl ); + Animator?.Simulate(); + } + + public override void BuildInput() + { + } + + public override void FrameSimulate( IClient cl ) + { + SimulateRotation(); + } + + public TraceResult TraceBBox( Vector3 start, Vector3 end, float liftFeet = 0.0f ) + { + return TraceBBox( start, end, Hull.Mins, Hull.Maxs, liftFeet ); + } + + public TraceResult TraceBBox( Vector3 start, Vector3 end, Vector3 mins, Vector3 maxs, float liftFeet = 0.0f ) + { + if ( liftFeet > 0 ) + { + start += Vector3.Up * liftFeet; + maxs = maxs.WithZ( maxs.z - liftFeet ); + } + + var tr = Trace.Ray( start, end ) + .Size( mins, maxs ) + .WithAnyTags( "solid", "playerclip", "passbullets" ) + .Ignore( this ) + .Run(); + + return tr; + } + + protected void SimulateRotation() + { + var idealRotation = ViewAngles.ToRotation(); + EyeRotation = Rotation.Slerp( Rotation, idealRotation, Time.Delta * 10f ); + Rotation = EyeRotation; + } +} diff --git a/code/Entities/RoundManager.cs b/code/Entities/RoundManager.cs new file mode 100644 index 0000000..2771b06 --- /dev/null +++ b/code/Entities/RoundManager.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LuckerGame.Enums; +using LuckerGame.Events; +using Sandbox; +using Sandbox.UI; + +namespace LuckerGame.Entities; + +/// +/// Manages rounds. Starting, stopping, triggering minigames from the manager, etc +/// +public partial class RoundManager : Entity +{ + /// + /// The minigame manager we should be using to manage minigames + /// + private MinigameManager MinigameManager { get; set; } + + /// + /// This percentage of the lobby must be ready to start the countdown for round start + /// + private const float RequiredReadyPercent = .5f; + + /// + /// The number of seconds from the timer starting before the round starts + /// + private const float RoundStartCountdownSeconds = 10f; + + /// + /// The state of the current round + /// + [Net] public RoundState RoundState { get; private set; } + + #region Countdown State + /// + /// How long since we started counting down to game start. Only useful if in the StartCountdown state. + /// + [Net] public TimeSince TimeSinceCountdownStarted { get; set; } + + /// + /// The number of seconds left in the countdown. Only useful if in the StartCountdown state. + /// + public int SecondsLeftInCountdown => (int)Math.Ceiling( RoundStartCountdownSeconds - TimeSinceCountdownStarted ); + #endregion + + #region In Progress State + + private const int MinigamesPerRound = 1; + + private int MinigamesLeftInRound { get; set; } + + private List Players { get; set; } + #endregion + + /// + public override void Spawn() + { + base.Spawn(); + RoundState = RoundState.NotStarted; + MinigameManager = new MinigameManager(); + } + + /// + /// Fires once per server tick + /// + [GameEvent.Tick.Server] + public void Tick() + { + if ( RoundState == RoundState.StartCountdown && TimeSinceCountdownStarted > RoundStartCountdownSeconds ) + { + Log.Info( "Starting round" ); + StartRound(); + } + } + + /// + /// Is triggered whenever a player readies up + /// + /// the player that readied up, discarded + [LuckerEvent.PlayerReady] + public void HandlePlayerReady( Lucker readyLucker, bool ready ) + { + if ( RoundState != RoundState.NotStarted && RoundState != RoundState.StartCountdown ) + { + return; + } + 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().ToList(); + var readiedCount = players.Count( player => player.Ready ); + var totalCount = players.Count; + if ( (float)readiedCount / totalCount > RequiredReadyPercent && RoundState == RoundState.NotStarted ) + { + Log.Info( "Countdown started" ); + RoundState = RoundState.StartCountdown; + TimeSinceCountdownStarted = 0; + } + else if ( (float)readiedCount / totalCount <= RequiredReadyPercent && RoundState == RoundState.StartCountdown ) + { + Log.Info( "Countdown ended" ); + RoundState = RoundState.NotStarted; + } + } + + private void StartRound() + { + if ( RoundState == RoundState.InProgress ) + { + Log.Warning( "Attempted to start round while one was in progress" ); + return; + } + + RoundState = RoundState.InProgress; + Players = All.OfType().ToList(); + MinigameManager.StartRandomMinigame( Players ); + } +} diff --git a/code/Entities/Weapons/Weapon.cs b/code/Entities/Weapons/Weapon.cs new file mode 100644 index 0000000..c1f6038 --- /dev/null +++ b/code/Entities/Weapons/Weapon.cs @@ -0,0 +1,250 @@ +using Sandbox; +using System.Collections.Generic; + +namespace LuckerGame.Entities.Weapons; + +public partial class Weapon : AnimatedEntity +{ + /// + /// The View Model's entity, only accessible clientside. + /// + public WeaponViewModel ViewModelEntity { get; protected set; } + + /// + /// An accessor to grab our Pawn. + /// + public Pawn Pawn => Owner as Pawn; + + /// + /// This'll decide which entity to fire effects from. If we're in first person, the View Model, otherwise, this. + /// + public AnimatedEntity EffectEntity => Camera.FirstPersonViewer == Owner ? ViewModelEntity : this; + + public virtual string ViewModelPath => null; + public virtual string ModelPath => null; + public virtual string ReloadSoundPath => null; + public virtual string ReloadAnimPath => "reload"; + public virtual string WeaponName => "weapon"; + public virtual float ReloadDuration => 4f; + private bool Reloading => TimeSinceReloadStarted < ReloadDuration; + + /// + /// How often you can shoot this gun. + /// + public virtual float PrimaryRate => 5.0f; + + /// + /// How long since we last shot this gun. + /// + [Net, Predicted] public TimeSince TimeSincePrimaryAttack { get; set; } + + [Net, Predicted] public TimeSince TimeSinceReloadStarted { get; set; } + + public virtual int MaxAmmo { get; } + + [Net] public int Ammo { get; set; } + + + public override void Spawn() + { + EnableHideInFirstPerson = true; + EnableShadowInFirstPerson = true; + EnableDrawing = false; + Ammo = MaxAmmo; + + if ( ModelPath != null ) + { + SetModel( ModelPath ); + } + } + + /// + /// Called when is called for this weapon. + /// + /// + public void OnEquip( Pawn pawn ) + { + Owner = pawn; + SetParent( pawn, true ); + EnableDrawing = true; + CreateViewModel( To.Single( pawn ) ); + } + + /// + /// Called when the weapon is either removed from the player, or holstered. + /// + public void OnHolster() + { + EnableDrawing = false; + DestroyViewModel( To.Single( Owner ) ); + } + + /// + /// Called from . + /// + /// + public override void Simulate( IClient player ) + { + Animate(); + + if ( CanPrimaryAttack() ) + { + using ( LagCompensation() ) + { + TimeSincePrimaryAttack = 0; + ReduceAmmoAndPrimaryAttack(); + } + } + else if ( CanReload() ) + { + Reload(); + } + } + + [ClientRpc] + protected virtual void ReloadEffects() + { + Game.AssertClient(); + ViewModelEntity?.SetAnimParameter( "reload", true ); + } + + private void Reload() + { + ReloadEffects(); + Pawn.PlaySound( ReloadSoundPath ); + Ammo = MaxAmmo; + Log.Info( $"Reload duration: {ReloadDuration}" ); + TimeSinceReloadStarted = 0; + } + + private bool CanReload() + { + return Owner.IsValid && Input.Down( "reload" ) && Ammo < MaxAmmo && !Reloading; + } + + /// + /// Called every to see if we can shoot our gun. + /// + /// + public virtual bool CanPrimaryAttack() + { + if ( !Owner.IsValid() || !Input.Down( "attack1" ) || Ammo == 0 || Reloading ) return false; + + var rate = PrimaryRate; + if ( rate <= 0 ) return true; + + return TimeSincePrimaryAttack > (1 / rate); + } + + private void ReduceAmmoAndPrimaryAttack() + { + Ammo--; + PrimaryAttack(); + } + + /// + /// Called when your gun shoots. + /// + public virtual void PrimaryAttack() + { + } + + /// + /// Useful for setting anim parameters based off the current weapon. + /// + protected virtual void Animate() + { + } + + /// + /// Does a trace from start to end, does bullet impact effects. Coded as an IEnumerable so you can return multiple + /// hits, like if you're going through layers or ricocheting or something. + /// + public virtual IEnumerable TraceBullet( Vector3 start, Vector3 end, float radius = 2.0f ) + { + bool underWater = Trace.TestPoint( start, "water" ); + + var trace = Trace.Ray( start, end ) + .UseHitboxes() + .WithAnyTags( "solid", "player", "npc" ) + .Ignore( this ) + .Size( radius ); + + // + // If we're not underwater then we can hit water + // + if ( !underWater ) + trace = trace.WithAnyTags( "water" ); + + var tr = trace.Run(); + + if ( tr.Hit ) + yield return tr; + } + + /// + /// Shoot a single bullet + /// + public virtual void ShootBullet( Vector3 pos, Vector3 dir, float spread, float force, float damage, float bulletSize ) + { + var forward = dir; + forward += (Vector3.Random + Vector3.Random + Vector3.Random + Vector3.Random) * spread * 0.25f; + forward = forward.Normal; + + // + // ShootBullet is coded in a way where we can have bullets pass through shit + // or bounce off shit, in which case it'll return multiple results + // + foreach ( var tr in TraceBullet( pos, pos + forward * 5000, bulletSize ) ) + { + tr.Surface.DoBulletImpact( tr ); + + if ( !Game.IsServer ) continue; + if ( !tr.Entity.IsValid() ) continue; + + // + // We turn predictiuon off for this, so any exploding effects don't get culled etc + // + using ( Prediction.Off() ) + { + var damageInfo = DamageInfo.FromBullet( tr.EndPosition, forward * 100 * force, damage ) + .UsingTraceResult( tr ) + .WithAttacker( Owner ) + .WithWeapon( this ); + + tr.Entity.TakeDamage( damageInfo ); + } + } + } + + /// + /// Shoot a single bullet from owners view point + /// + public virtual void ShootBullet( float spread, float force, float damage, float bulletSize ) + { + Game.SetRandomSeed( Time.Tick ); + + var ray = Owner.AimRay; + ShootBullet( ray.Position, ray.Forward, spread, force, damage, bulletSize ); + } + + [ClientRpc] + public void CreateViewModel() + { + if ( ViewModelPath == null ) return; + + var vm = new WeaponViewModel( this ); + vm.Model = Model.Load( ViewModelPath ); + ViewModelEntity = vm; + } + + [ClientRpc] + public void DestroyViewModel() + { + if ( ViewModelEntity.IsValid() ) + { + ViewModelEntity.Delete(); + } + } +} + diff --git a/code/Entities/Weapons/WeaponViewModel.cs b/code/Entities/Weapons/WeaponViewModel.cs new file mode 100644 index 0000000..782c7f3 --- /dev/null +++ b/code/Entities/Weapons/WeaponViewModel.cs @@ -0,0 +1,22 @@ +using Sandbox; + +namespace LuckerGame.Entities.Weapons; + +public partial class WeaponViewModel : BaseViewModel +{ + protected Weapon Weapon { get; init; } + + public WeaponViewModel( Weapon weapon ) + { + Weapon = weapon; + EnableShadowCasting = false; + EnableViewmodelRendering = true; + } + + public override void PlaceViewmodel() + { + base.PlaceViewmodel(); + + Camera.Main.SetViewModelCamera( 80f, 1, 500 ); + } +} diff --git a/code/EntityComponents/Client/ClientComponent.cs b/code/EntityComponents/Client/ClientComponent.cs new file mode 100644 index 0000000..e9c720a --- /dev/null +++ b/code/EntityComponents/Client/ClientComponent.cs @@ -0,0 +1,11 @@ +using Sandbox; + +namespace LuckerGame.Components.Client; + +/// +/// Intended to be a base class for components intended for IClient, as IClient cannot be used as an EntityComponent type param +/// +public abstract class ClientComponent : EntityComponent +{ + public IClient Client => Entity as IClient; +} diff --git a/code/EntityComponents/Lucker/Cameras/AbstractCamera.cs b/code/EntityComponents/Lucker/Cameras/AbstractCamera.cs new file mode 100644 index 0000000..d30de0f --- /dev/null +++ b/code/EntityComponents/Lucker/Cameras/AbstractCamera.cs @@ -0,0 +1,36 @@ +using LuckerGame.Entities; +using Sandbox; + +namespace LuckerGame.Components.Lucker.Cameras; + +public abstract class AbstractCamera : EntityComponent +{ + protected Vector3 CameraPosition { get; set; } + protected Rotation CameraRotation { get; set; } + protected float FieldOfView { get; set; } + protected IEntity FirstPersonViewer { get; set; } + protected Transform SoundSource { get; set; } + + /// + /// Handles any input-independent camera updates (ie following a pawn) + /// + protected abstract void UpdateCameraParameters(); + + /// + /// Handles any input dependent camera updates (ie moving around a top down cam) + /// + public abstract void BuildInput(); + + /// + /// Applies Camera parameters to the static Camera + /// + public virtual void Update() + { + UpdateCameraParameters(); + Camera.Position = CameraPosition; + Camera.Rotation = CameraRotation; + Camera.FieldOfView = FieldOfView; + Camera.FirstPersonViewer = FirstPersonViewer; + Sound.Listener = SoundSource; + } +} diff --git a/code/EntityComponents/Lucker/Cameras/RTSCamera.cs b/code/EntityComponents/Lucker/Cameras/RTSCamera.cs new file mode 100644 index 0000000..4d5d905 --- /dev/null +++ b/code/EntityComponents/Lucker/Cameras/RTSCamera.cs @@ -0,0 +1,47 @@ +using Sandbox; +using System; +using System.Linq; + +namespace LuckerGame.Components.Lucker.Cameras; + +/// +/// A top down camera that can be +/// +public partial class RTSCamera : AbstractCamera, ISingletonComponent +{ + private const float MaxDistance = 400f; + private const float MinDistance = 200f; + private const float Speed = 4f; + protected float WheelSpeed => 5f; + protected Vector2 DistanceClamp => new( MinDistance, MaxDistance ); + protected float CameraDistance = MinDistance; + protected float TargetDistance = MinDistance; + + private Vector3 CameraPositionOffset = Vector3.Zero; + + protected override void UpdateCameraParameters() + { + CameraPosition = Entity.Position + CameraPositionOffset; + CameraRotation = Rotation.FromPitch( -270 ); + FieldOfView = 70f; + FirstPersonViewer = null; + CameraPosition = CameraPosition.WithZ( CameraDistance ); + SoundSource = new Transform { Position = CameraPosition }; + } + + public override void BuildInput() + { + // Zooming in and out + var wheel = Input.MouseWheel; + if ( wheel != 0 ) + { + TargetDistance -= wheel * WheelSpeed; + TargetDistance = TargetDistance.Clamp( DistanceClamp.x, DistanceClamp.y ); + } + + CameraDistance = CameraDistance.LerpTo( TargetDistance, Time.Delta * 10f ); + + // Moving + CameraPositionOffset += Input.AnalogMove * Speed; + } +} diff --git a/code/EntityComponents/Lucker/Cameras/TopDownCamera.cs b/code/EntityComponents/Lucker/Cameras/TopDownCamera.cs new file mode 100644 index 0000000..28c253f --- /dev/null +++ b/code/EntityComponents/Lucker/Cameras/TopDownCamera.cs @@ -0,0 +1,97 @@ +/*using Sandbox; +using System; +using System.Linq; + +namespace LuckerGame.Components.Lucker.Cameras; + +/// +/// A top downish camera that follows an entity +/// +public partial class TopDownCamera : AbstractCamera, ISingletonComponent +{ + protected float WheelSpeed => 30f; + protected Vector2 CameraDistance => new( 125, 1000 ); + protected Vector2 PitchClamp => new( 30, 60 ); + + float OrbitDistance = 400f; + float TargetOrbitDistance = 400f; + Angles OrbitAngles = Angles.Zero; + + protected static Vector3 IntersectPlane( Vector3 pos, Vector3 dir, float z ) + { + float a = (z - pos.z) / dir.z; + return new( dir.x * a + pos.x, dir.y * a + pos.y, z ); + } + + protected static Rotation LookAt( Vector3 targetPosition, Vector3 position ) + { + var targetDelta = (targetPosition - position); + var direction = targetDelta.Normal; + + return Rotation.From( new Angles( + ((float)Math.Asin( direction.z )).RadianToDegree() * -1.0f, + ((float)Math.Atan2( direction.y, direction.x )).RadianToDegree(), + 0.0f ) ); + } + + public override void Update() + { + var pawn = Entity; + if ( !pawn.IsValid() ) + return; + + Camera.Position = pawn.Position; + Vector3 targetPos; + + Camera.Position += Vector3.Up * (pawn.CollisionBounds.Center.z * pawn.Scale); + Camera.Rotation = Rotation.From( OrbitAngles ); + + targetPos = Camera.Position + Camera.Rotation.Backward * OrbitDistance; + + Camera.Position = targetPos; + Camera.FieldOfView = 70f; + Camera.FirstPersonViewer = null; + + Sound.Listener = new() + { + Position = pawn.AimRay.Position, + Rotation = pawn.EyeRotation + }; + } + + public override void BuildInput() + { + var wheel = Input.MouseWheel; + if ( wheel != 0 ) + { + TargetOrbitDistance -= wheel * WheelSpeed; + TargetOrbitDistance = TargetOrbitDistance.Clamp( CameraDistance.x, CameraDistance.y ); + } + + OrbitDistance = OrbitDistance.LerpTo( TargetOrbitDistance, Time.Delta * 10f ); + + // Sets pawn to face where the camera faces + if ( Input.UsingController || Input.Down( "attack2" ) ) + { + OrbitAngles.yaw += Input.AnalogLook.yaw; + OrbitAngles.pitch += Input.AnalogLook.pitch; + OrbitAngles = OrbitAngles.Normal; + + Entity.ViewAngles = OrbitAngles.WithPitch( 0f ); + } + // Sets pawn to face to point that the mouse is over + else + { + // Get ray from direction vector from mouse pointer through screen + var direction = Screen.GetDirection( Mouse.Position, Camera.FieldOfView, Camera.Rotation, Screen.Size ); + + var hitPos = IntersectPlane( Camera.Position, direction, Entity.EyePosition.z ); + + Entity.ViewAngles = (hitPos - Entity.EyePosition).EulerAngles; + } + + OrbitAngles.pitch = OrbitAngles.pitch.Clamp( PitchClamp.x, PitchClamp.y ); + + Entity.InputDirection = Input.AnalogMove; + } +}*/ diff --git a/code/EntityComponents/Lucker/LuckerClientInput.cs b/code/EntityComponents/Lucker/LuckerClientInput.cs new file mode 100644 index 0000000..0f0575d --- /dev/null +++ b/code/EntityComponents/Lucker/LuckerClientInput.cs @@ -0,0 +1,15 @@ +using Sandbox; + +namespace LuckerGame.EntityComponents.Lucker; + +/// +/// A component for capturing and passing around a client's input for an attached Lucker +/// +public class LuckerClientInput : EntityComponent +{ + [ClientInput] + public Vector3 InputDirection { get; set; } + + [ClientInput] + public Angles ViewAngles { get; set; } +} diff --git a/code/EntityComponents/Pawn/PawnAnimator.cs b/code/EntityComponents/Pawn/PawnAnimator.cs new file mode 100644 index 0000000..84d91fe --- /dev/null +++ b/code/EntityComponents/Pawn/PawnAnimator.cs @@ -0,0 +1,21 @@ +using Sandbox; +using System; + +namespace LuckerGame.Components.Pawn; + +public class PawnAnimator : EntityComponent, ISingletonComponent +{ + public void Simulate() + { + var helper = new CitizenAnimationHelper( Entity ); + helper.WithVelocity( Entity.Velocity ); + helper.WithLookAt( Entity.EyePosition + Entity.EyeRotation.Forward * 100 ); + helper.HoldType = CitizenAnimationHelper.HoldTypes.None; + helper.IsGrounded = Entity.GroundEntity.IsValid(); + + if ( Entity.Controller.HasEvent( "jump" ) ) + { + helper.TriggerJump(); + } + } +} diff --git a/code/EntityComponents/Pawn/UserPawnController.cs b/code/EntityComponents/Pawn/UserPawnController.cs new file mode 100644 index 0000000..cd84a52 --- /dev/null +++ b/code/EntityComponents/Pawn/UserPawnController.cs @@ -0,0 +1,174 @@ +using Sandbox; +using System; +using System.Collections.Generic; + +namespace LuckerGame.Components.Pawn; + +public class UserPawnController : EntityComponent +{ + public int StepSize => 24; + public int GroundAngle => 45; + public int JumpSpeed => 410; + public float Gravity => 800f; + + HashSet ControllerEvents = new( StringComparer.OrdinalIgnoreCase ); + + bool Grounded => Entity.GroundEntity.IsValid(); + + public void Simulate( IClient cl ) + { + ControllerEvents.Clear(); + + var movement = Entity.InputDirection.Normal; + var angles = Camera.Rotation.Angles().WithPitch( 0 ); + var moveVector = Rotation.From( angles ) * movement * 320f; + var groundEntity = CheckForGround(); + + if ( groundEntity.IsValid() ) + { + if ( !Grounded ) + { + Entity.Velocity = Entity.Velocity.WithZ( 0 ); + AddEvent( "grounded" ); + } + + Entity.Velocity = Accelerate( Entity.Velocity, moveVector.Normal, moveVector.Length, 200.0f * ( Input.Down( "run" ) ? 2.5f : 1f ), 7.5f ); + Entity.Velocity = ApplyFriction( Entity.Velocity, 4.0f ); + } + else + { + Entity.Velocity = Accelerate( Entity.Velocity, moveVector.Normal, moveVector.Length, 15, 20f ); + Entity.Velocity += Vector3.Down * Gravity * Time.Delta; + } + + if ( Input.Pressed( "jump" ) ) + { + DoJump(); + } + + var mh = new MoveHelper( Entity.Position, Entity.Velocity ); + mh.Trace = mh.Trace.Size( Entity.Hull ).Ignore( Entity ); + + if ( mh.TryMoveWithStep( Time.Delta, StepSize ) > 0 ) + { + if ( Grounded ) + { + mh.Position = StayOnGround( mh.Position ); + } + Entity.Position = mh.Position; + Entity.Velocity = mh.Velocity; + } + + Entity.GroundEntity = groundEntity; + } + + void DoJump() + { + if ( Grounded ) + { + Entity.Velocity = ApplyJump( Entity.Velocity, "jump" ); + } + } + + Entity CheckForGround() + { + if ( Entity.Velocity.z > 300f ) + return null; + + var trace = Entity.TraceBBox( Entity.Position, Entity.Position + Vector3.Down, 2f ); + + if ( !trace.Hit ) + return null; + + if ( trace.Normal.Angle( Vector3.Up ) > GroundAngle ) + return null; + + return trace.Entity; + } + + Vector3 ApplyFriction( Vector3 input, float frictionAmount ) + { + float StopSpeed = 100.0f; + + var speed = input.Length; + if ( speed < 0.1f ) return input; + + // Bleed off some speed, but if we have less than the bleed + // threshold, bleed the threshold amount. + float control = (speed < StopSpeed) ? StopSpeed : speed; + + // Add the amount to the drop amount. + var drop = control * Time.Delta * frictionAmount; + + // scale the velocity + float newspeed = speed - drop; + if ( newspeed < 0 ) newspeed = 0; + if ( newspeed == speed ) return input; + + newspeed /= speed; + input *= newspeed; + + return input; + } + + Vector3 Accelerate( Vector3 input, Vector3 wishdir, float wishspeed, float speedLimit, float acceleration ) + { + if ( speedLimit > 0 && wishspeed > speedLimit ) + wishspeed = speedLimit; + + var currentspeed = input.Dot( wishdir ); + var addspeed = wishspeed - currentspeed; + + if ( addspeed <= 0 ) + return input; + + var accelspeed = acceleration * Time.Delta * wishspeed; + + if ( accelspeed > addspeed ) + accelspeed = addspeed; + + input += wishdir * accelspeed; + + return input; + } + + Vector3 ApplyJump( Vector3 input, string jumpType ) + { + AddEvent( jumpType ); + + return input + Vector3.Up * JumpSpeed; + } + + Vector3 StayOnGround( Vector3 position ) + { + var start = position + Vector3.Up * 2; + var end = position + Vector3.Down * StepSize; + + // See how far up we can go without getting stuck + var trace = Entity.TraceBBox( position, start ); + start = trace.EndPosition; + + // Now trace down from a known safe position + trace = Entity.TraceBBox( start, end ); + + if ( trace.Fraction <= 0 ) return position; + if ( trace.Fraction >= 1 ) return position; + if ( trace.StartedSolid ) return position; + if ( Vector3.GetAngle( Vector3.Up, trace.Normal ) > GroundAngle ) return position; + + return trace.EndPosition; + } + + public bool HasEvent( string eventName ) + { + return ControllerEvents.Contains( eventName ); + } + + void AddEvent( string eventName ) + { + if ( HasEvent( eventName ) ) + return; + + ControllerEvents.Add( eventName ); + } +} diff --git a/code/Enums/RoundState.cs b/code/Enums/RoundState.cs new file mode 100644 index 0000000..2d0178b --- /dev/null +++ b/code/Enums/RoundState.cs @@ -0,0 +1,20 @@ +namespace LuckerGame.Enums; + +/// +/// The state of a current round of minigames +/// +public enum RoundState +{ + /// + /// This round has not yet started + /// + NotStarted, + /// + /// This round is counting down to start + /// + StartCountdown, + /// + /// This round is currently in progress + /// + InProgress +} diff --git a/code/Events/MinigameEndEvent.cs b/code/Events/MinigameEndEvent.cs new file mode 100644 index 0000000..5e09651 --- /dev/null +++ b/code/Events/MinigameEndEvent.cs @@ -0,0 +1,18 @@ +using Sandbox; + +namespace LuckerGame.Events; + +public static partial class LuckerEvent +{ + public const string MinigameEnd = "lucker.minigameEnd"; + + /// + /// Event is run on the server whenever a minigame ends + /// + public class MinigameEndAttribute : EventAttribute + { + public MinigameEndAttribute() : base(MinigameEnd) + { + } + } +} diff --git a/code/Events/PlayerReadyEvent.cs b/code/Events/PlayerReadyEvent.cs new file mode 100644 index 0000000..2bfde23 --- /dev/null +++ b/code/Events/PlayerReadyEvent.cs @@ -0,0 +1,20 @@ +using Sandbox; + +namespace LuckerGame.Events; + +public static partial class LuckerEvent +{ + public const string PlayerReady = "lucker.playerReady"; + + /// + /// 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 + /// + [MethodArguments(typeof(Entities.Lucker), typeof(bool))] + public class PlayerReadyAttribute : EventAttribute + { + public PlayerReadyAttribute() : base(PlayerReady) + { + } + } +} diff --git a/code/Game.cs b/code/Game.cs new file mode 100644 index 0000000..5e470c1 --- /dev/null +++ b/code/Game.cs @@ -0,0 +1,69 @@ + +using Sandbox; +using System; +using System.Linq; +using LuckerGame.Components.Client; +using LuckerGame.UI; +using LuckerGame.Entities; +using Sandbox.UI; + +// +// You don't need to put things in a namespace, but it doesn't hurt. +// +namespace LuckerGame; + +/// +/// This is your game class. This is an entity that is created serverside when +/// the game starts, and is replicated to the client. +/// +/// You can use this to create things like HUDs and declare which player class +/// to use for spawned players. +/// +public partial class LuckerGameManager : GameManager +{ + /// + /// Called when the game is created (on both the server and client) + /// + public LuckerGameManager() + { + if ( Game.IsClient ) + { + Game.RootPanel = new Hud(); + } + else if ( Game.IsServer ) + { + var roundManager = new RoundManager(); + } + } + + public override void PostLevelLoaded() + { + base.PostLevelLoaded(); + } + + /// + /// A client has joined the server. Make them a pawn to play with + /// + public override void ClientJoined( IClient client ) + { + base.ClientJoined( client ); + + // Create a pawn for this client to play with + var lucker = Entities.Lucker.CreateLuckerForClient( client ); + + // Get all of the spawnpoints + var spawnpoints = Entity.All.OfType(); + + // chose a random one + var randomSpawnPoint = spawnpoints.OrderBy( x => Guid.NewGuid() ).FirstOrDefault(); + + // if it exists, place the pawn there + if ( randomSpawnPoint != null ) + { + var tx = randomSpawnPoint.Transform; + tx.Position = tx.Position + Vector3.Up * 50.0f; // raise it up + lucker.Position = tx.Position; + } + } +} + diff --git a/code/Minigames/Minigame.cs b/code/Minigames/Minigame.cs new file mode 100644 index 0000000..d814282 --- /dev/null +++ b/code/Minigames/Minigame.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using LuckerGame.Entities; +using Sandbox; + +namespace LuckerGame.Minigames; + +public abstract class Minigame : Entity +{ + public abstract string Name { get; } + /// + /// Initializes the minigame with a list of luckers playing it + /// + /// the players who made it into the minigame + public abstract void Initialize(List players); + + /// + /// Cleans up any entities and components created by this minigame. + /// It is not necessary to remove the lucker's pawns, the manager will do so if any were assigned. + /// + public abstract void Cleanup(); +} diff --git a/code/Minigames/RussianRoulette/RussianRouletteMinigame.cs b/code/Minigames/RussianRoulette/RussianRouletteMinigame.cs new file mode 100644 index 0000000..a2397cc --- /dev/null +++ b/code/Minigames/RussianRoulette/RussianRouletteMinigame.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LuckerGame.Entities; +using Sandbox; + +namespace LuckerGame.Minigames.RussianRoulette; + +public class RussianRouletteMinigame : Minigame +{ + public override string Name => "Russian Roulette"; + + public override void Initialize( List players ) + { + var pawn = new Pawn(); + + // Get all of the spawnpoints + var spawnpoints = Entity.All.OfType(); + + // chose a random one + var randomSpawnPoint = spawnpoints.OrderBy( x => Guid.NewGuid() ).FirstOrDefault(); + + // if it exists, place the pawn there + if ( randomSpawnPoint != null ) + { + var tx = randomSpawnPoint.Transform; + tx.Position = tx.Position + Vector3.Up * 50.0f; // raise it up + pawn.Position = tx.Position; + } + } + + public override void Cleanup() + { + throw new System.NotImplementedException(); + } +} diff --git a/code/UI/Hud.razor b/code/UI/Hud.razor new file mode 100644 index 0000000..8322108 --- /dev/null +++ b/code/UI/Hud.razor @@ -0,0 +1,27 @@ +@using Sandbox; +@using Sandbox.UI; +@using LuckerGame.UI.HudComponents +@using LuckerGame.UI.Menus + +@namespace LuckerGame.UI +@inherits RootPanel +@attribute [StyleSheet] + + + + + + + +
+ + + +@code +{ + public override void Tick() + { + var devCam = Game.LocalClient.Components.Get(); + SetClass( "camera-movement", Input.UsingController || Input.Down( "attack2" ) || devCam is not null ); + } +} \ No newline at end of file diff --git a/code/UI/Hud.razor.scss b/code/UI/Hud.razor.scss new file mode 100644 index 0000000..2efe07d --- /dev/null +++ b/code/UI/Hud.razor.scss @@ -0,0 +1,30 @@ +$primary-color: 0, 123, 160; +$primary-color-translucent: rgba($primary-color, 0.7); +Hud +{ +} +.primary-color-background { + background-color: rgb($primary-color); +} +.primary-color-translucent-background { + background-color: $primary-color-translucent; +} +label +{ + font-family: Poppins; + color: white; + font-size: 32px; + + &.subtitle + { + font-size: 16px; + } + + &.header { + font-size: 48px; + } + + &.material-icon { + font-family: "Material Icons"; + } +} diff --git a/code/UI/HudComponents/Scoreboard.razor b/code/UI/HudComponents/Scoreboard.razor new file mode 100644 index 0000000..113d83c --- /dev/null +++ b/code/UI/HudComponents/Scoreboard.razor @@ -0,0 +1,29 @@ +@using System +@using System.Collections.Generic +@using System.Linq +@using LuckerGame.Entities +@using Sandbox +@using Sandbox.UI +@namespace LuckerGame.UI.HudComponents +@attribute [StyleSheet] +@inherits Panel + + +
+ @foreach (var player in Luckers) + { + + } +
+
+ +@code { + + private IReadOnlyCollection Luckers => Entity.All.OfType().ToList(); + + protected override int BuildHash() + { + return HashCode.Combine(Luckers.Select(player => player.Name).ToList()); + } + +} \ No newline at end of file diff --git a/code/UI/HudComponents/Scoreboard.razor.scss b/code/UI/HudComponents/Scoreboard.razor.scss new file mode 100644 index 0000000..e1211f3 --- /dev/null +++ b/code/UI/HudComponents/Scoreboard.razor.scss @@ -0,0 +1,3 @@ +Scoreboard { + z-index: 0; +} \ No newline at end of file diff --git a/code/UI/MenuComponents/LuckerButton.razor b/code/UI/MenuComponents/LuckerButton.razor new file mode 100644 index 0000000..716f2e6 --- /dev/null +++ b/code/UI/MenuComponents/LuckerButton.razor @@ -0,0 +1,26 @@ +@using System +@using Sandbox.Razor +@using Sandbox.UI + +@inherits Panel +@attribute [StyleSheet] +@namespace LuckerGame.UI.MenuComponents + + +
+ @ChildContent +
+
+ +@code { + + private RenderFragment _childContent; + + public RenderFragment ChildContent + { + get { return _childContent; } + set { _childContent = value; StateHasChanged(); } + } + + +} \ No newline at end of file diff --git a/code/UI/MenuComponents/LuckerButton.razor.scss b/code/UI/MenuComponents/LuckerButton.razor.scss new file mode 100644 index 0000000..92d27b4 --- /dev/null +++ b/code/UI/MenuComponents/LuckerButton.razor.scss @@ -0,0 +1,15 @@ +LuckerButton { + div { + &.lucker-button { + z-index: 11; + cursor: pointer; + background-color: gray; + padding: 5px; + border-radius: 10px; + + :hover { + opacity: 0.7; + } + } + } +} \ No newline at end of file diff --git a/code/UI/MenuComponents/LuckerSpinner.razor b/code/UI/MenuComponents/LuckerSpinner.razor new file mode 100644 index 0000000..51f260a --- /dev/null +++ b/code/UI/MenuComponents/LuckerSpinner.razor @@ -0,0 +1,9 @@ +@using Sandbox.UI + +@namespace LuckerGame.UI.MenuComponents +@inherits Panel +@attribute [StyleSheet] + + +
+ \ No newline at end of file diff --git a/code/UI/MenuComponents/LuckerSpinner.razor.scss b/code/UI/MenuComponents/LuckerSpinner.razor.scss new file mode 100644 index 0000000..adba57e --- /dev/null +++ b/code/UI/MenuComponents/LuckerSpinner.razor.scss @@ -0,0 +1,20 @@ +LuckerSpinner { + .loader { + width: 48px; + height: 48px; + border: 5px solid #FFF; + border-bottom-color: transparent; + border-radius: 50%; + animation-name: rotation; + animation-duration: 1s; + animation-iteration-count: infinite; + } +} +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/code/UI/Menus/VotingLobby.razor b/code/UI/Menus/VotingLobby.razor new file mode 100644 index 0000000..3a1bf8e --- /dev/null +++ b/code/UI/Menus/VotingLobby.razor @@ -0,0 +1,77 @@ +@using System +@using System.Collections.Generic +@using System.Linq +@using LuckerGame.Entities +@using LuckerGame.Enums +@using Sandbox.UI; +@using Sandbox; +@using LuckerGame.UI.MenuComponents + +@inherits Panel +@attribute [StyleSheet] +@namespace LuckerGame.UI.Menus +@if (RoundManager == null) +{ + +
+ +
+
+ return; +} +@if (RoundManager.RoundState == RoundState.InProgress) +{ + return; +} + +
+ @if (RoundManager.RoundState == RoundState.NotStarted) + { + + } + else + { + if (@RoundManager.SecondsLeftInCountdown > 0) + { + + } + else + { + + } + } +
+ @foreach (var lucker in Luckers) + { +
+ +
+ } +
+ + + + + +
+
+ +@code { + + private List Luckers => Entity.All.OfType().ToList(); + private RoundManager RoundManager => Entity.All.OfType().FirstOrDefault(); + private Lucker ThisLucker => Game.LocalClient.Pawn as Lucker; + private bool Ready => ThisLucker.Ready; + + protected override int BuildHash() + { + return HashCode.Combine(Luckers.Select(lucker => lucker.Ready), Ready, RoundManager?.SecondsLeftInCountdown); + } + + private void ReadyButtonPressed() + { + ConsoleSystem.Run("set_ready", !ThisLucker.Ready); + StateHasChanged(); + } + +} \ No newline at end of file diff --git a/code/UI/Menus/VotingLobby.razor.scss b/code/UI/Menus/VotingLobby.razor.scss new file mode 100644 index 0000000..e7345db --- /dev/null +++ b/code/UI/Menus/VotingLobby.razor.scss @@ -0,0 +1,43 @@ +VotingLobby { + z-index: 10; + .label { + text-align: center; + } + .voting-panel { + gap: 10px; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + backdrop-filter: blur(10px); + width: 100vw; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + pointer-events: all; + + .header { + position: absolute; + top: 0; + } + LuckerButton { + position: absolute; + bottom: 0; + margin-bottom: 10px; + } + } + .voters { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + .voter { + width: 100vw; + .label { + width: 100%; + } + } +} \ No newline at end of file