├── .addon ├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── code ├── Game.cs ├── Systems │ ├── Player │ │ ├── Controller │ │ │ ├── Mechanics │ │ │ │ ├── AirMove.cs │ │ │ │ ├── BaseMechanic.cs │ │ │ │ ├── Crouch.cs │ │ │ │ ├── Interaction.cs │ │ │ │ ├── Jump.cs │ │ │ │ ├── Sprint.cs │ │ │ │ └── Walk.cs │ │ │ └── PlayerController.cs │ │ ├── Inventory.cs │ │ ├── Player.Clothing.cs │ │ ├── Player.GameEvents.cs │ │ ├── Player.Input.cs │ │ ├── Player.Ragdoll.cs │ │ ├── Player.cs │ │ ├── PlayerAnimator.cs │ │ └── PlayerCamera.cs │ └── Weapon │ │ ├── Components │ │ ├── PrimaryFireComponent.cs │ │ ├── ViewModelComponent.cs │ │ └── WeaponComponent.cs │ │ ├── Weapon.Components.cs │ │ ├── Weapon.cs │ │ ├── WeaponViewModel.Effects.cs │ │ └── WeaponViewModel.cs └── UI │ ├── Chat.cs │ ├── Chat.razor │ ├── ChatRow.razor │ ├── Crosshair.razor │ ├── Hud.razor │ ├── Info.razor │ ├── Players.razor │ └── StyleSheets │ ├── _chat.scss │ ├── _hud.scss │ ├── _info.scss │ └── _players.scss └── prefabs ├── pistol.prefab ├── pistol.prefab_c ├── smg.prefab └── smg.prefab_c /.addon: -------------------------------------------------------------------------------- 1 | { 2 | "Title": "FPS Sample", 3 | "Type": "game", 4 | "Org": "local", 5 | "Ident": "fps_sample", 6 | "Tags": "", 7 | "Schema": 1, 8 | "HasAssets": true, 9 | "AssetsPath": "", 10 | "ResourcePaths": [], 11 | "HasCode": true, 12 | "CodePath": "/code/", 13 | "PackageReferences": [], 14 | "EditorReferences": null, 15 | "Metadata": { 16 | "MaxPlayers": 64, 17 | "MinPlayers": 1, 18 | "GameNetworkType": "Multiplayer", 19 | "MapSelect": "Unrestricted", 20 | "MapList": [ 21 | "gkaf.motel" 22 | ], 23 | "RankType": "None", 24 | "PerMapRanking": false, 25 | "LeaderboardType": "None", 26 | "Summary": "A first person shooter sample project.", 27 | "Description": "", 28 | "Public": true, 29 | "ControlModes": { 30 | "Keyboard": true, 31 | "Gamepad": true 32 | }, 33 | "Collision": { 34 | "Defaults": { 35 | "solid": "Collide", 36 | "trigger": "Trigger", 37 | "ladder": "Ignore", 38 | "water": "Trigger", 39 | "player": "Collide" 40 | }, 41 | "Pairs": [ 42 | { 43 | "a": "solid", 44 | "b": "solid", 45 | "r": "Collide" 46 | }, 47 | { 48 | "a": "trigger", 49 | "b": "playerclip", 50 | "r": "Ignore" 51 | }, 52 | { 53 | "a": "trigger", 54 | "b": "solid", 55 | "r": "Trigger" 56 | }, 57 | { 58 | "a": "solid", 59 | "b": "trigger", 60 | "r": "Collide" 61 | }, 62 | { 63 | "a": "playerclip", 64 | "b": "solid", 65 | "r": "Collide" 66 | }, 67 | { 68 | "a": "player", 69 | "b": "player", 70 | "r": "Trigger" 71 | } 72 | ] 73 | }, 74 | "ProjectTemplate": { 75 | "Icon": "directions_walk", 76 | "Order": -12 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | root = true 3 | 4 | # C# files 5 | [*.cs] 6 | indent_style = tab 7 | indent_size = 4 8 | tab_size = 4 9 | 10 | # New line preferences 11 | end_of_line = crlf 12 | insert_final_newline = true 13 | 14 | 15 | #### C# Coding Conventions #### 16 | 17 | # Expression-bodied members 18 | csharp_style_expression_bodied_accessors = true:silent 19 | csharp_style_expression_bodied_constructors = false:silent 20 | csharp_style_expression_bodied_indexers = true:silent 21 | csharp_style_expression_bodied_lambdas = true:silent 22 | csharp_style_expression_bodied_local_functions = false:silent 23 | csharp_style_expression_bodied_methods = false:silent 24 | csharp_style_expression_bodied_operators = false:silent 25 | csharp_style_expression_bodied_properties = true:silent 26 | 27 | # Pattern matching preferences 28 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 29 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 30 | csharp_style_prefer_not_pattern = true:suggestion 31 | csharp_style_prefer_pattern_matching = true:silent 32 | csharp_style_prefer_switch_expression = true:suggestion 33 | 34 | # Null-checking preferences 35 | csharp_style_conditional_delegate_call = true:suggestion 36 | 37 | # Code-block preferences 38 | csharp_prefer_braces = true:silent 39 | 40 | # Expression-level preferences 41 | csharp_prefer_simple_default_expression = true:suggestion 42 | csharp_style_deconstructed_variable_declaration = true:suggestion 43 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion 44 | csharp_style_inlined_variable_declaration = true:suggestion 45 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 46 | csharp_style_prefer_index_operator = true:suggestion 47 | csharp_style_prefer_range_operator = true:suggestion 48 | csharp_style_throw_expression = true:suggestion 49 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 50 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 51 | 52 | # 'using' directive preferences 53 | csharp_using_directive_placement = outside_namespace:silent 54 | 55 | #### C# Formatting Rules #### 56 | 57 | # New line preferences 58 | csharp_new_line_before_catch = true 59 | csharp_new_line_before_else = true 60 | csharp_new_line_before_finally = true 61 | csharp_new_line_before_members_in_anonymous_types = true 62 | csharp_new_line_before_members_in_object_initializers = true 63 | csharp_new_line_before_open_brace = all 64 | csharp_new_line_between_query_expression_clauses = true 65 | 66 | # Indentation preferences 67 | csharp_indent_block_contents = true 68 | csharp_indent_braces = false 69 | csharp_indent_case_contents = true 70 | csharp_indent_case_contents_when_block = true 71 | csharp_indent_labels = no_change 72 | csharp_indent_switch_labels = true 73 | 74 | # Space preferences 75 | csharp_space_after_cast = false 76 | csharp_space_after_colon_in_inheritance_clause = true 77 | csharp_space_after_comma = true 78 | csharp_space_after_dot = false 79 | csharp_space_after_keywords_in_control_flow_statements = true 80 | csharp_space_after_semicolon_in_for_statement = true 81 | csharp_space_around_binary_operators = before_and_after 82 | csharp_space_around_declaration_statements = false 83 | csharp_space_before_colon_in_inheritance_clause = true 84 | csharp_space_before_comma = false 85 | csharp_space_before_dot = false 86 | csharp_space_before_open_square_brackets = false 87 | csharp_space_before_semicolon_in_for_statement = false 88 | csharp_space_between_empty_square_brackets = false 89 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 90 | csharp_space_between_method_call_name_and_opening_parenthesis = false 91 | csharp_space_between_method_call_parameter_list_parentheses = true 92 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 93 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 94 | csharp_space_between_method_declaration_parameter_list_parentheses = true 95 | csharp_space_between_parentheses = control_flow_statements 96 | csharp_space_between_square_brackets = false 97 | 98 | # Wrapping preferences 99 | csharp_preserve_single_line_blocks = true 100 | csharp_preserve_single_line_statements = true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Sandbox 2 | code/*.csproj 3 | code/obj 4 | code/Properties 5 | .intermediate 6 | _bakeresourcecache 7 | tools_asset_info.bin 8 | tools_thumbnail_cache.bin 9 | *.los 10 | .vs/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022, 2023 Facepunch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FPS Sample Project 2 | A first person sample project made for s&box. 3 | 4 | ![Screenshot](https://files.facepunch.com/devultj/1b1811b1/sbox-dev_XokWjXbMhI.jpg "Screenshot") 5 | 6 | ## Features 7 | - Mechanic driven first person player controller 8 | - Prefabs, component based weapons 9 | - Examples of [Razor](https://wiki.facepunch.com/sbox/ui-razor) usage for game UI 10 | - Simple weapon inventory 11 | 12 | ## Contributions 13 | I'm not currently looking for contributions to the project, unless it's something small, like bug fixes. 14 | 15 | ## Credits 16 | - Tony Ferguson - [@DevulTj](https://github.com/devultj) 17 | -------------------------------------------------------------------------------- /code/Game.cs: -------------------------------------------------------------------------------- 1 | global using Sandbox; 2 | global using Sandbox.UI; 3 | global using System; 4 | global using System.Linq; 5 | global using System.Collections.Generic; 6 | global using System.ComponentModel; 7 | 8 | // 9 | global using GameTemplate.UI; 10 | 11 | // 12 | // You don't need to put things in a namespace, but it doesn't hurt. 13 | // 14 | namespace GameTemplate; 15 | 16 | /// 17 | /// This is your game class. This is an entity that is created serverside when 18 | /// the game starts, and is replicated to the client. 19 | /// 20 | /// You can use this to create things like HUDs and declare which player class 21 | /// to use for spawned players. 22 | /// 23 | public partial class TemplateGameManager : GameManager 24 | { 25 | public TemplateGameManager() 26 | { 27 | if ( Game.IsClient ) 28 | { 29 | Game.RootPanel = new Hud(); 30 | } 31 | else 32 | { 33 | Game.TickRate = 30; 34 | } 35 | } 36 | 37 | /// 38 | /// A client has joined the server. Make them a pawn to play with 39 | /// 40 | public override void ClientJoined( IClient client ) 41 | { 42 | base.ClientJoined( client ); 43 | 44 | // Create a pawn for this client to play with 45 | var pawn = new Player(); 46 | client.Pawn = pawn; 47 | pawn.Respawn(); 48 | 49 | // Get all of the spawnpoints 50 | var spawnpoints = Entity.All.OfType(); 51 | 52 | // chose a random one 53 | var randomSpawnPoint = spawnpoints.OrderBy( x => Guid.NewGuid() ).FirstOrDefault(); 54 | 55 | // if it exists, place the pawn there 56 | if ( randomSpawnPoint != null ) 57 | { 58 | var tx = randomSpawnPoint.Transform; 59 | tx.Position = tx.Position + Vector3.Up * 10.0f; // raise it up 60 | pawn.Transform = tx; 61 | } 62 | 63 | Chat.AddChatEntry( To.Everyone, client.Name, "joined the game", client.SteamId, true ); 64 | } 65 | 66 | public override void ClientDisconnect( IClient client, NetworkDisconnectionReason reason ) 67 | { 68 | base.ClientDisconnect( client, reason ); 69 | Chat.AddChatEntry( To.Everyone, client.Name, "left the game", client.SteamId, true ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /code/Systems/Player/Controller/Mechanics/AirMove.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate.Mechanics; 2 | 3 | /// 4 | /// AirMove decides how the player moves while in the air. Drives effects such as gravity, wind, etc. 5 | /// 6 | public partial class AirMoveMechanic : PlayerControllerMechanic 7 | { 8 | public float Gravity => 800.0f; 9 | public float AirControl => 30.0f; 10 | public float AirAcceleration => 35.0f; 11 | 12 | protected override void Simulate() 13 | { 14 | var ctrl = Controller; 15 | ctrl.Velocity -= new Vector3( 0, 0, Gravity * 0.5f ) * Time.Delta; 16 | ctrl.Velocity += new Vector3( 0, 0, ctrl.BaseVelocity.z ) * Time.Delta; 17 | ctrl.BaseVelocity = ctrl.BaseVelocity.WithZ( 0 ); 18 | 19 | var groundedAtStart = GroundEntity.IsValid(); 20 | if ( groundedAtStart ) 21 | return; 22 | 23 | var wishVel = ctrl.GetWishVelocity( true ); 24 | var wishdir = wishVel.Normal; 25 | var wishspeed = wishVel.Length; 26 | 27 | ctrl.Accelerate( wishdir, wishspeed, AirControl, AirAcceleration ); 28 | ctrl.Velocity += ctrl.BaseVelocity; 29 | ctrl.Move(); 30 | ctrl.Velocity -= ctrl.BaseVelocity; 31 | ctrl.Velocity -= new Vector3( 0, 0, Gravity * 0.5f ) * Time.Delta; 32 | } 33 | 34 | // AirMove should always simulate 35 | protected override bool ShouldStart() => true; 36 | } 37 | -------------------------------------------------------------------------------- /code/Systems/Player/Controller/Mechanics/BaseMechanic.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate.Mechanics; 2 | 3 | public partial class PlayerControllerMechanic : EntityComponent 4 | { 5 | /// 6 | /// Is this mechanic active? 7 | /// 8 | public bool IsActive { get; protected set; } 9 | 10 | /// 11 | /// How long has it been since we activated this mechanic? 12 | /// 13 | public TimeSince TimeSinceStart { get; protected set; } 14 | 15 | /// 16 | /// How long has it been since we deactivated this mechanic? 17 | /// 18 | public TimeSince TimeSinceStop { get; protected set; } 19 | 20 | /// 21 | /// Standard cooldown for mechanics. 22 | /// 23 | public TimeUntil TimeUntilCanStart { get; protected set; } 24 | 25 | protected PlayerController Controller => Entity.Controller; 26 | 27 | /// 28 | /// Accessor for the player. 29 | /// 30 | protected Player Player => Controller.Player; 31 | 32 | /// 33 | /// Used to dictate the most important mechanic to take information such as EyeHeight, WishSpeed. 34 | /// 35 | public virtual int SortOrder { get; set; } = 0; 36 | 37 | /// 38 | /// Override the current eye height. 39 | /// 40 | public virtual float? EyeHeight { get; set; } = null; 41 | 42 | /// 43 | /// Override the current wish speed. 44 | /// 45 | public virtual float? WishSpeed { get; set; } = null; 46 | 47 | /// 48 | /// Identifier for the Mechanic 49 | /// 50 | public virtual new string Name => info.Name.Trim(); 51 | 52 | public Vector3 Position 53 | { 54 | get => Controller.Position; 55 | set => Controller.Position = value; 56 | } 57 | 58 | public Vector3 Velocity 59 | { 60 | get => Controller.Velocity; 61 | set => Controller.Velocity = value; 62 | } 63 | 64 | public Vector3 LastVelocity 65 | { 66 | get => Controller.LastVelocity; 67 | set => Controller.LastVelocity = value; 68 | } 69 | 70 | public Entity GroundEntity 71 | { 72 | get => Controller.GroundEntity; 73 | set => Controller.GroundEntity = value; 74 | } 75 | 76 | public Entity LastGroundEntity 77 | { 78 | get => Controller.LastGroundEntity; 79 | set => Controller.LastGroundEntity = value; 80 | } 81 | 82 | /// 83 | /// Mechanics can override friction - the Walk mechanic drives this. 84 | /// 85 | public virtual float? FrictionOverride { get; set; } = null; 86 | 87 | /// 88 | /// Lets you override the movement input scale. 89 | /// Dividing this by 2 would make the player move twice as slow. 90 | /// 91 | public virtual Vector3? MoveInputScale { get; set; } = null; 92 | 93 | DisplayInfo info; 94 | public PlayerControllerMechanic() 95 | { 96 | info = DisplayInfo.For( this ); 97 | } 98 | 99 | /// 100 | /// Called every time the controller simulates, for each mechanic. 101 | /// 102 | /// 103 | /// 104 | public bool TrySimulate( PlayerController controller ) 105 | { 106 | var before = IsActive; 107 | IsActive = ShouldStart(); 108 | 109 | if ( IsActive ) 110 | { 111 | if ( before != IsActive ) 112 | { 113 | Start(); 114 | } 115 | 116 | Simulate(); 117 | } 118 | // Deactivate 119 | if ( before && !IsActive ) 120 | { 121 | Stop(); 122 | } 123 | 124 | return IsActive; 125 | } 126 | 127 | protected void Start() 128 | { 129 | TimeSinceStart = 0; 130 | RunGameEvent( $"{Name}.start" ); 131 | OnStart(); 132 | } 133 | 134 | protected void Stop() 135 | { 136 | TimeSinceStop = 0; 137 | RunGameEvent( $"{Name}.stop" ); 138 | OnStop(); 139 | } 140 | 141 | /// 142 | /// Called when the mechanic deactivates. For example, when you stop crouching. 143 | /// 144 | protected virtual void OnStop() 145 | { 146 | // 147 | } 148 | 149 | /// 150 | /// Called when the mechanic activates. For example, when you start sliding. 151 | /// 152 | protected virtual void OnStart() 153 | { 154 | // 155 | } 156 | 157 | /// 158 | /// Returns whether or not this ability should activate and simulate this tick. 159 | /// By default, it's set to TimeUntilCanNextActivate, which you can set in your own mechanics. 160 | /// 161 | /// 162 | protected virtual bool ShouldStart() 163 | { 164 | return TimeUntilCanStart; 165 | } 166 | 167 | /// 168 | /// Runs every Simulate **only** if the mechanic isn't active. 169 | /// 170 | protected virtual void Simulate() 171 | { 172 | // 173 | } 174 | 175 | /// 176 | /// Runs every tick even if the mechanic isn't active. 177 | /// 178 | public virtual void Tick() 179 | { 180 | 181 | } 182 | 183 | public override string ToString() 184 | { 185 | return $"{info.Name}: IsActive({IsActive})"; 186 | } 187 | 188 | protected WallInfo GetWallInfo( Vector3 direction ) 189 | { 190 | var trace = Controller.TraceBBox( Controller.Position, Controller.Position + direction * 32f ); 191 | if ( !trace.Hit ) return default; 192 | 193 | Vector3 tracePos; 194 | var height = ApproximateWallHeight( Controller.Position, trace.Normal, 500f, 32f, 128, out tracePos, out float absoluteHeight ); 195 | 196 | return new WallInfo() 197 | { 198 | Hit = true, 199 | Height = height, 200 | AbsoluteHeight = absoluteHeight, 201 | Distance = trace.Distance, 202 | Normal = trace.Normal, 203 | Trace = trace, 204 | TracePos = tracePos, 205 | }; 206 | } 207 | 208 | private static int MaxWallTraceIterations => 40; 209 | private static float ApproximateWallHeight( Vector3 startPos, Vector3 wallNormal, float maxHeight, float maxDist, int precision, out Vector3 tracePos, out float absoluteHeight ) 210 | { 211 | tracePos = Vector3.Zero; 212 | absoluteHeight = startPos.z; 213 | 214 | var step = maxHeight / precision; 215 | 216 | float currentHeight = 0f; 217 | var foundWall = false; 218 | for ( int i = 0; i < Math.Min( precision, MaxWallTraceIterations ); i++ ) 219 | { 220 | startPos.z += step; 221 | currentHeight += step; 222 | var trace = Trace.Ray( startPos, startPos - wallNormal * maxDist ) 223 | .WorldOnly() 224 | .Run(); 225 | 226 | if ( PlayerController.Debug ) 227 | DebugOverlay.TraceResult( trace ); 228 | 229 | if ( !trace.Hit && !foundWall ) continue; 230 | if ( trace.Hit ) 231 | { 232 | tracePos = trace.HitPosition; 233 | 234 | foundWall = true; 235 | continue; 236 | } 237 | 238 | absoluteHeight = startPos.z; 239 | return currentHeight; 240 | } 241 | return 0f; 242 | } 243 | 244 | /// 245 | /// Called when a game event is sent to the player. 246 | /// 247 | /// 248 | public virtual void OnGameEvent( string eventName ) 249 | { 250 | // 251 | } 252 | 253 | /// 254 | /// Accessor to quickly run a player game event. 255 | /// 256 | /// 257 | public void RunGameEvent( string eventName ) 258 | { 259 | Player?.RunGameEvent( eventName ); 260 | } 261 | 262 | public struct WallInfo 263 | { 264 | public bool Hit; 265 | public float Distance; 266 | public Vector3 Normal; 267 | public float Height; 268 | public float AbsoluteHeight; 269 | public TraceResult Trace; 270 | public Vector3 TracePos; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /code/Systems/Player/Controller/Mechanics/Crouch.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate.Mechanics; 2 | 3 | /// 4 | /// The basic crouch mechanic for players. 5 | /// 6 | public partial class CrouchMechanic : PlayerControllerMechanic 7 | { 8 | public override int SortOrder => 9; 9 | public override float? WishSpeed => 120f; 10 | public override float? EyeHeight => 40f; 11 | 12 | protected override bool ShouldStart() 13 | { 14 | if ( !Input.Down( "duck" ) ) return false; 15 | if ( !Controller.GroundEntity.IsValid() ) return false; 16 | if ( Controller.IsMechanicActive() ) return false; 17 | 18 | return true; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /code/Systems/Player/Controller/Mechanics/Interaction.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate.Mechanics; 2 | 3 | public partial class InteractionMechanic : PlayerControllerMechanic, ISingletonComponent 4 | { 5 | /// 6 | /// Entity the player is currently using via their interaction key. 7 | /// 8 | public Entity Using { get; protected set; } 9 | 10 | protected virtual void TickUse() 11 | { 12 | // This is serverside only 13 | if ( !Game.IsServer ) return; 14 | 15 | // Turn prediction off 16 | using ( Prediction.Off() ) 17 | { 18 | if ( Input.Pressed( "use" ) ) 19 | { 20 | Using = FindUsable(); 21 | 22 | if ( Using == null ) 23 | { 24 | UseFail(); 25 | return; 26 | } 27 | } 28 | 29 | if ( !Input.Down( "use" ) ) 30 | { 31 | StopUsing(); 32 | return; 33 | } 34 | 35 | if ( !Using.IsValid() ) 36 | return; 37 | 38 | // If we move too far away or something we should probably ClearUse()? 39 | 40 | // 41 | // If use returns true then we can keep using it 42 | // 43 | if ( Using is IUse use && use.OnUse( Entity ) ) 44 | return; 45 | 46 | StopUsing(); 47 | } 48 | } 49 | 50 | /// 51 | /// Player tried to use something but there was nothing there. 52 | /// Tradition is to give a disappointed boop. 53 | /// 54 | protected virtual void UseFail() 55 | { 56 | Entity.PlaySound( "player_use_fail" ); 57 | } 58 | 59 | /// 60 | /// If we're using an entity, stop using it 61 | /// 62 | protected virtual void StopUsing() 63 | { 64 | Using = null; 65 | } 66 | 67 | /// 68 | /// Returns if the entity is a valid usable entity 69 | /// 70 | protected bool IsValidUseEntity( Entity e ) 71 | { 72 | if ( e == null ) return false; 73 | 74 | if ( e is IInteractable interactable ) 75 | if ( !interactable.IsUsable( Entity ) ) return false; 76 | 77 | if ( e is IUse usable ) 78 | if ( !usable.IsUsable( Entity ) ) return false; 79 | 80 | return true; 81 | } 82 | 83 | /// 84 | /// Find a usable entity for this player to use 85 | /// 86 | protected virtual Entity FindUsable() 87 | { 88 | var eyePosition = Entity.AimRay.Position; 89 | var eyeForward = Entity.AimRay.Forward; 90 | 91 | // First try a direct 0 width line 92 | var tr = Trace.Ray( eyePosition, eyePosition + eyeForward * 85 ) 93 | .Ignore( Entity ) 94 | .Run(); 95 | 96 | // See if any of the parent entities are usable if we ain't. 97 | var ent = tr.Entity; 98 | while ( ent.IsValid() && !IsValidUseEntity( ent ) ) 99 | { 100 | ent = ent.Parent; 101 | } 102 | 103 | // Nothing found, try a wider search 104 | if ( !IsValidUseEntity( ent ) ) 105 | { 106 | tr = Trace.Ray( eyePosition, eyePosition + eyeForward * 85 ) 107 | .Radius( 2 ) 108 | .Ignore( Entity ) 109 | .Run(); 110 | 111 | // See if any of the parent entities are usable if we ain't. 112 | ent = tr.Entity; 113 | while ( ent.IsValid() && !IsValidUseEntity( ent ) ) 114 | { 115 | ent = ent.Parent; 116 | } 117 | } 118 | 119 | if ( !IsValidUseEntity( ent ) ) return null; 120 | 121 | return ent; 122 | } 123 | 124 | protected override void Simulate() 125 | { 126 | TickUse(); 127 | } 128 | 129 | /// 130 | /// Describes an interactable object. 131 | /// 132 | public interface IInteractable 133 | { 134 | bool OnUse( Player player ); 135 | bool IsUsable( Player player ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /code/Systems/Player/Controller/Mechanics/Jump.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate.Mechanics; 2 | 3 | /// 4 | /// The jump mechanic for players. 5 | /// 6 | public partial class JumpMechanic : PlayerControllerMechanic 7 | { 8 | public override int SortOrder => 25; 9 | private float Gravity => 700f; 10 | 11 | protected override bool ShouldStart() 12 | { 13 | if ( !Input.Pressed( "jump" ) ) return false; 14 | if ( !Controller.GroundEntity.IsValid() ) return false; 15 | return true; 16 | } 17 | 18 | protected override void OnStart() 19 | { 20 | float flGroundFactor = 1.0f; 21 | float flMul = 250f; 22 | float startz = Velocity.z; 23 | 24 | Velocity = Velocity.WithZ( startz + flMul * flGroundFactor ); 25 | Velocity -= new Vector3( 0, 0, Gravity * 0.5f ) * Time.Delta; 26 | 27 | Controller.GetMechanic() 28 | .ClearGroundEntity(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /code/Systems/Player/Controller/Mechanics/Sprint.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate.Mechanics; 2 | 3 | /// 4 | /// The basic sprinting mechanic for players. 5 | /// It shouldn't, though. 6 | /// 7 | public partial class SprintMechanic : PlayerControllerMechanic 8 | { 9 | /// 10 | /// Sprint has a higher priority than other mechanics. 11 | /// 12 | public override int SortOrder => 10; 13 | public override float? WishSpeed => 320f; 14 | 15 | protected override bool ShouldStart() 16 | { 17 | if ( !Input.Down( "run" ) ) return false; 18 | if ( Player.MoveInput.Length == 0 ) return false; 19 | 20 | return true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /code/Systems/Player/Controller/Mechanics/Walk.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate.Mechanics; 2 | 3 | /// 4 | /// The basic walking mechanic for the player. 5 | /// 6 | public partial class WalkMechanic : PlayerControllerMechanic 7 | { 8 | public float StopSpeed => 150f; 9 | public float StepSize => 18.0f; 10 | public float GroundAngle => 46.0f; 11 | public float DefaultSpeed => 280f; 12 | public float WalkSpeed => 140f; 13 | public float GroundFriction => 4.0f; 14 | public float MaxNonJumpVelocity => 140.0f; 15 | public float SurfaceFriction { get; set; } = 1f; 16 | public float Acceleration => 6f; 17 | public float DuckAcceleration => 5f; 18 | 19 | protected override bool ShouldStart() 20 | { 21 | return true; 22 | } 23 | 24 | public override float? WishSpeed => 200f; 25 | 26 | protected override void Simulate() 27 | { 28 | if ( GroundEntity != null ) 29 | WalkMove(); 30 | 31 | CategorizePosition( Controller.GroundEntity != null ); 32 | } 33 | 34 | /// 35 | /// Try to keep a walking player on the ground when running down slopes etc. 36 | /// 37 | private void StayOnGround() 38 | { 39 | var start = Controller.Position + Vector3.Up * 2; 40 | var end = Controller.Position + Vector3.Down * StepSize; 41 | 42 | // See how far up we can go without getting stuck 43 | var trace = Controller.TraceBBox( Controller.Position, start ); 44 | start = trace.EndPosition; 45 | 46 | // Now trace down from a known safe position 47 | trace = Controller.TraceBBox( start, end ); 48 | 49 | if ( trace.Fraction <= 0 ) return; 50 | if ( trace.Fraction >= 1 ) return; 51 | if ( trace.StartedSolid ) return; 52 | if ( Vector3.GetAngle( Vector3.Up, trace.Normal ) > GroundAngle ) return; 53 | 54 | Controller.Position = trace.EndPosition; 55 | } 56 | 57 | private void WalkMove() 58 | { 59 | var ctrl = Controller; 60 | 61 | var wishVel = ctrl.GetWishVelocity( true ); 62 | var wishdir = wishVel.Normal; 63 | var wishspeed = wishVel.Length; 64 | var friction = GroundFriction * SurfaceFriction; 65 | 66 | ctrl.Velocity = ctrl.Velocity.WithZ( 0 ); 67 | ctrl.ApplyFriction( StopSpeed, friction ); 68 | 69 | var accel = Acceleration; 70 | 71 | ctrl.Velocity = ctrl.Velocity.WithZ( 0 ); 72 | ctrl.Accelerate( wishdir, wishspeed, 0, accel ); 73 | ctrl.Velocity = ctrl.Velocity.WithZ( 0 ); 74 | 75 | // Add in any base velocity to the current velocity. 76 | ctrl.Velocity += ctrl.BaseVelocity; 77 | 78 | try 79 | { 80 | if ( ctrl.Velocity.Length < 1.0f ) 81 | { 82 | ctrl.Velocity = Vector3.Zero; 83 | return; 84 | } 85 | 86 | var dest = (ctrl.Position + ctrl.Velocity * Time.Delta).WithZ( ctrl.Position.z ); 87 | var pm = ctrl.TraceBBox( ctrl.Position, dest ); 88 | 89 | if ( pm.Fraction == 1 ) 90 | { 91 | ctrl.Position = pm.EndPosition; 92 | StayOnGround(); 93 | return; 94 | } 95 | 96 | ctrl.StepMove(); 97 | } 98 | finally 99 | { 100 | ctrl.Velocity -= ctrl.BaseVelocity; 101 | } 102 | 103 | StayOnGround(); 104 | } 105 | 106 | /// 107 | /// We're no longer on the ground, remove it 108 | /// 109 | public void ClearGroundEntity() 110 | { 111 | if ( GroundEntity == null ) return; 112 | 113 | LastGroundEntity = GroundEntity; 114 | GroundEntity = null; 115 | SurfaceFriction = 1.0f; 116 | } 117 | 118 | public void SetGroundEntity( Entity entity ) 119 | { 120 | LastGroundEntity = GroundEntity; 121 | LastVelocity = Velocity; 122 | 123 | GroundEntity = entity; 124 | 125 | if ( GroundEntity != null ) 126 | { 127 | Velocity = Velocity.WithZ( 0 ); 128 | Controller.BaseVelocity = GroundEntity.Velocity; 129 | } 130 | } 131 | 132 | public void CategorizePosition( bool bStayOnGround ) 133 | { 134 | SurfaceFriction = 1.0f; 135 | 136 | var point = Position - Vector3.Up * 2; 137 | var vBumpOrigin = Position; 138 | bool bMovingUpRapidly = Velocity.z > MaxNonJumpVelocity; 139 | bool bMoveToEndPos = false; 140 | 141 | if ( GroundEntity != null ) 142 | { 143 | bMoveToEndPos = true; 144 | point.z -= StepSize; 145 | } 146 | else if ( bStayOnGround ) 147 | { 148 | bMoveToEndPos = true; 149 | point.z -= StepSize; 150 | } 151 | 152 | if ( bMovingUpRapidly ) 153 | { 154 | ClearGroundEntity(); 155 | return; 156 | } 157 | 158 | var pm = Controller.TraceBBox( vBumpOrigin, point, 4.0f ); 159 | 160 | var angle = Vector3.GetAngle( Vector3.Up, pm.Normal ); 161 | Controller.CurrentGroundAngle = angle; 162 | 163 | if ( pm.Entity == null || Vector3.GetAngle( Vector3.Up, pm.Normal ) > GroundAngle ) 164 | { 165 | ClearGroundEntity(); 166 | bMoveToEndPos = false; 167 | 168 | if ( Velocity.z > 0 ) 169 | SurfaceFriction = 0.25f; 170 | } 171 | else 172 | { 173 | UpdateGroundEntity( pm ); 174 | } 175 | 176 | if ( bMoveToEndPos && !pm.StartedSolid && pm.Fraction > 0.0f && pm.Fraction < 1.0f ) 177 | { 178 | Position = pm.EndPosition; 179 | } 180 | } 181 | 182 | private void UpdateGroundEntity( TraceResult tr ) 183 | { 184 | Controller.GroundNormal = tr.Normal; 185 | 186 | SurfaceFriction = tr.Surface.Friction * 1.25f; 187 | if ( SurfaceFriction > 1 ) SurfaceFriction = 1; 188 | 189 | SetGroundEntity( tr.Entity ); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /code/Systems/Player/Controller/PlayerController.cs: -------------------------------------------------------------------------------- 1 | using GameTemplate.Mechanics; 2 | 3 | namespace GameTemplate; 4 | 5 | public partial class PlayerController : EntityComponent, ISingletonComponent 6 | { 7 | public Vector3 LastVelocity { get; set; } 8 | public Entity LastGroundEntity { get; set; } 9 | public Entity GroundEntity { get; set; } 10 | public Vector3 BaseVelocity { get; set; } 11 | public Vector3 GroundNormal { get; set; } 12 | public float CurrentGroundAngle { get; set; } 13 | 14 | public Player Player => Entity; 15 | 16 | /// 17 | /// A list of mechanics used by the player controller. 18 | /// 19 | public IEnumerable Mechanics => Entity.Components.GetAll(); 20 | 21 | /// 22 | /// Position accessor for the Player. 23 | /// 24 | public Vector3 Position 25 | { 26 | get => Player.Position; 27 | set => Player.Position = value; 28 | } 29 | 30 | public Vector3 Velocity 31 | { 32 | get => Player.Velocity; 33 | set => Player.Velocity = value; 34 | } 35 | 36 | /// 37 | /// This'll set LocalEyePosition when we Simulate. 38 | /// 39 | public float EyeHeight => BestMechanic?.EyeHeight ?? 64f; 40 | 41 | [Net, Predicted] public float CurrentEyeHeight { get; set; } = 64f; 42 | 43 | public Vector3 MoveInputScale => BestMechanic?.MoveInputScale ?? Vector3.One; 44 | 45 | /// 46 | /// The "best" mechanic is the mechanic with the highest priority, defined by SortOrder. 47 | /// 48 | public PlayerControllerMechanic BestMechanic => Mechanics.OrderByDescending( x => x.SortOrder ).FirstOrDefault( x => x.IsActive ); 49 | 50 | [ConVar.Replicated( "playercontroller_debug" )] 51 | public static bool Debug { get; set; } = false; 52 | 53 | public float BodyGirth => 32f; 54 | 55 | /// 56 | /// The player's hull, we'll use this to calculate stuff like collision. 57 | /// 58 | public BBox Hull 59 | { 60 | get 61 | { 62 | var girth = BodyGirth * 0.5f; 63 | var baseHeight = CurrentEyeHeight; 64 | 65 | var mins = new Vector3( -girth, -girth, 0 ); 66 | var maxs = new Vector3( +girth, +girth, baseHeight * 1.1f ); 67 | 68 | return new BBox( mins, maxs ); 69 | } 70 | } 71 | 72 | public T GetMechanic() where T : PlayerControllerMechanic 73 | { 74 | foreach ( var mechanic in Mechanics ) 75 | { 76 | if ( mechanic is T val ) return val; 77 | } 78 | 79 | return null; 80 | } 81 | 82 | public bool IsMechanicActive() where T : PlayerControllerMechanic 83 | { 84 | return GetMechanic()?.IsActive ?? false; 85 | } 86 | 87 | protected void SimulateEyes() 88 | { 89 | var target = EyeHeight; 90 | // Magic number :sad: 91 | var trace = TraceBBox( Position, Position, 0, 10f ); 92 | if ( trace.Hit && target > CurrentEyeHeight ) 93 | { 94 | // We hit something, that means we can't increase our eye height because something's in the way. 95 | } 96 | else 97 | { 98 | CurrentEyeHeight = CurrentEyeHeight.LerpTo( target, Time.Delta * 10f ); 99 | } 100 | 101 | Player.EyeRotation = Player.LookInput.ToRotation(); 102 | Player.EyeLocalPosition = Vector3.Up * CurrentEyeHeight; 103 | } 104 | 105 | protected void SimulateMechanics() 106 | { 107 | foreach ( var mechanic in Mechanics ) 108 | { 109 | mechanic.TrySimulate( this ); 110 | 111 | // All mechanics can tick. 112 | mechanic.Tick(); 113 | } 114 | } 115 | 116 | public virtual void FrameSimulate( IClient cl ) 117 | { 118 | SimulateEyes(); 119 | } 120 | 121 | public virtual void Simulate( IClient cl ) 122 | { 123 | SimulateEyes(); 124 | SimulateMechanics(); 125 | 126 | if ( Debug ) 127 | { 128 | var hull = Hull; 129 | DebugOverlay.Box( Position, hull.Mins, hull.Maxs, Color.Red ); 130 | DebugOverlay.Box( Position, hull.Mins, hull.Maxs, Color.Blue ); 131 | 132 | var lineOffset = 50; 133 | 134 | DebugOverlay.ScreenText( $"Player Controller", ++lineOffset ); 135 | DebugOverlay.ScreenText( $" Position: {Position}", ++lineOffset ); 136 | DebugOverlay.ScreenText( $" Velocity: {Velocity}", ++lineOffset ); 137 | DebugOverlay.ScreenText( $" BaseVelocity: {BaseVelocity}", ++lineOffset ); 138 | DebugOverlay.ScreenText( $" GroundEntity: {GroundEntity} [{GroundEntity?.Velocity}]", ++lineOffset ); 139 | DebugOverlay.ScreenText( $" Speed: {Velocity.Length}", ++lineOffset ); 140 | 141 | ++lineOffset; 142 | DebugOverlay.ScreenText( $"Mechanics", ++lineOffset ); 143 | foreach ( var mechanic in Mechanics ) 144 | { 145 | DebugOverlay.ScreenText( $"{mechanic}", ++lineOffset ); 146 | } 147 | } 148 | } 149 | 150 | /// 151 | /// Traces the bbox and returns the trace result. 152 | /// LiftFeet will move the start position up by this amount, while keeping the top of the bbox at the same 153 | /// position. This is good when tracing down because you won't be tracing through the ceiling above. 154 | /// 155 | public virtual TraceResult TraceBBox( Vector3 start, Vector3 end, Vector3 mins, Vector3 maxs, float liftFeet = 0.0f, float liftHead = 0.0f ) 156 | { 157 | if ( liftFeet > 0 ) 158 | { 159 | start += Vector3.Up * liftFeet; 160 | maxs = maxs.WithZ( maxs.z - liftFeet ); 161 | } 162 | 163 | if ( liftHead > 0 ) 164 | { 165 | end += Vector3.Up * liftHead; 166 | } 167 | 168 | var tr = Trace.Ray( start, end ) 169 | .Size( mins, maxs ) 170 | .WithAnyTags( "solid", "playerclip", "passbullets" ) 171 | .Ignore( Player ) 172 | .Run(); 173 | 174 | return tr; 175 | } 176 | 177 | /// 178 | /// This calls TraceBBox with the right sized bbox. You should derive this in your controller if you 179 | /// want to use the built in functions 180 | /// 181 | public virtual TraceResult TraceBBox( Vector3 start, Vector3 end, float liftFeet = 0.0f, float liftHead = 0.0f ) 182 | { 183 | var hull = Hull; 184 | return TraceBBox( start, end, hull.Mins, hull.Maxs, liftFeet, liftHead ); 185 | } 186 | 187 | public Vector3 GetWishVelocity( bool zeroPitch = false ) 188 | { 189 | var result = new Vector3( Player.MoveInput.x, Player.MoveInput.y, 0 ); 190 | result *= MoveInputScale; 191 | 192 | var inSpeed = result.Length.Clamp( 0, 1 ); 193 | result *= Player.LookInput.WithPitch( 0f ).ToRotation(); 194 | 195 | if ( zeroPitch ) 196 | result.z = 0; 197 | 198 | result = result.Normal * inSpeed; 199 | result *= GetWishSpeed(); 200 | 201 | var ang = CurrentGroundAngle.Remap( 0, 45, 1, 0.6f ); 202 | result *= ang; 203 | 204 | return result; 205 | } 206 | 207 | public virtual float GetWishSpeed() 208 | { 209 | return BestMechanic?.WishSpeed ?? 180f; 210 | } 211 | 212 | public void Accelerate( Vector3 wishdir, float wishspeed, float speedLimit, float acceleration ) 213 | { 214 | if ( speedLimit > 0 && wishspeed > speedLimit ) 215 | wishspeed = speedLimit; 216 | 217 | var currentspeed = Velocity.Dot( wishdir ); 218 | var addspeed = wishspeed - currentspeed; 219 | 220 | if ( addspeed <= 0 ) 221 | return; 222 | 223 | var accelspeed = acceleration * Time.Delta * wishspeed; 224 | 225 | if ( accelspeed > addspeed ) 226 | accelspeed = addspeed; 227 | 228 | Velocity += wishdir * accelspeed; 229 | } 230 | 231 | public void ApplyFriction( float stopSpeed, float frictionAmount = 1.0f ) 232 | { 233 | var speed = Velocity.Length; 234 | if ( speed.AlmostEqual( 0f ) ) return; 235 | 236 | if ( BestMechanic?.FrictionOverride != null ) 237 | frictionAmount = BestMechanic.FrictionOverride.Value; 238 | 239 | var control = (speed < stopSpeed) ? stopSpeed : speed; 240 | var drop = control * Time.Delta * frictionAmount; 241 | 242 | // Scale the velocity 243 | float newspeed = speed - drop; 244 | if ( newspeed < 0 ) newspeed = 0; 245 | 246 | if ( newspeed != speed ) 247 | { 248 | newspeed /= speed; 249 | Velocity *= newspeed; 250 | } 251 | } 252 | 253 | public void StepMove( float groundAngle = 46f, float stepSize = 18f ) 254 | { 255 | MoveHelper mover = new MoveHelper( Position, Velocity ); 256 | mover.Trace = mover.Trace.Size( Hull ) 257 | .Ignore( Player ) 258 | .WithoutTags( "player" ); 259 | mover.MaxStandableAngle = groundAngle; 260 | 261 | mover.TryMoveWithStep( Time.Delta, stepSize ); 262 | 263 | Position = mover.Position; 264 | Velocity = mover.Velocity; 265 | } 266 | 267 | public void Move( float groundAngle = 46f ) 268 | { 269 | MoveHelper mover = new MoveHelper( Position, Velocity ); 270 | mover.Trace = mover.Trace.Size( Hull ) 271 | .Ignore( Player ) 272 | .WithoutTags( "player" ); 273 | mover.MaxStandableAngle = groundAngle; 274 | 275 | mover.TryMove( Time.Delta ); 276 | 277 | Position = mover.Position; 278 | Velocity = mover.Velocity; 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /code/Systems/Player/Inventory.cs: -------------------------------------------------------------------------------- 1 | using GameTemplate.Weapons; 2 | 3 | namespace GameTemplate; 4 | 5 | /// 6 | /// The player's inventory holds a player's weapons, and holds the player's current weapon. 7 | /// It also drives functionality such as weapon switching. 8 | /// 9 | public partial class Inventory : EntityComponent, ISingletonComponent 10 | { 11 | [Net] public IList Weapons { get; set; } 12 | [Net, Predicted] public Weapon ActiveWeapon { get; set; } 13 | 14 | public bool AddWeapon( Weapon weapon, bool makeActive = true ) 15 | { 16 | if ( Weapons.Contains( weapon ) ) return false; 17 | 18 | Weapons.Add( weapon ); 19 | 20 | if ( makeActive ) 21 | SetActiveWeapon( weapon ); 22 | 23 | return true; 24 | } 25 | 26 | public bool RemoveWeapon( Weapon weapon, bool drop = false ) 27 | { 28 | var success = Weapons.Remove( weapon ); 29 | if ( success && drop ) 30 | { 31 | // TODO - Drop the weapon on the ground 32 | } 33 | 34 | return success; 35 | } 36 | 37 | public void SetActiveWeapon( Weapon weapon ) 38 | { 39 | var currentWeapon = ActiveWeapon; 40 | if ( currentWeapon.IsValid() ) 41 | { 42 | // Can reject holster if we're doing an action already 43 | if ( !currentWeapon.CanHolster( Entity ) ) 44 | { 45 | return; 46 | } 47 | 48 | currentWeapon.OnHolster( Entity ); 49 | ActiveWeapon = null; 50 | } 51 | 52 | // Can reject deploy if we're doing an action already 53 | if ( !weapon.CanDeploy( Entity ) ) 54 | { 55 | return; 56 | } 57 | 58 | ActiveWeapon = weapon; 59 | 60 | weapon?.OnDeploy( Entity ); 61 | } 62 | 63 | protected override void OnDeactivate() 64 | { 65 | if ( Game.IsServer ) 66 | { 67 | Weapons.ToList() 68 | .ForEach( x => x.Delete() ); 69 | } 70 | } 71 | 72 | public Weapon GetSlot( int slot ) 73 | { 74 | return Weapons.ElementAtOrDefault( slot ) ?? null; 75 | } 76 | 77 | protected int GetSlotIndexFromInput( string slot ) 78 | { 79 | return slot switch 80 | { 81 | "slot1" => 0, 82 | "slot2" => 1, 83 | "slot3" => 2, 84 | "slot4" => 3, 85 | "slot5" => 4, 86 | _ => -1 87 | }; 88 | } 89 | 90 | protected void TrySlotFromInput( string slot ) 91 | { 92 | if ( Input.Pressed( slot ) ) 93 | { 94 | Input.ReleaseAction( slot ); 95 | 96 | if ( GetSlot( GetSlotIndexFromInput( slot ) ) is Weapon weapon ) 97 | { 98 | Entity.ActiveWeaponInput = weapon; 99 | } 100 | } 101 | } 102 | 103 | public void BuildInput() 104 | { 105 | TrySlotFromInput( "slot1" ); 106 | TrySlotFromInput( "slot2" ); 107 | TrySlotFromInput( "slot3" ); 108 | TrySlotFromInput( "slot4" ); 109 | TrySlotFromInput( "slot5" ); 110 | 111 | ActiveWeapon?.BuildInput(); 112 | } 113 | 114 | public void Simulate( IClient cl ) 115 | { 116 | if ( Entity.ActiveWeaponInput != null && ActiveWeapon != Entity.ActiveWeaponInput ) 117 | { 118 | SetActiveWeapon( Entity.ActiveWeaponInput as Weapon ); 119 | Entity.ActiveWeaponInput = null; 120 | } 121 | 122 | ActiveWeapon?.Simulate( cl ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /code/Systems/Player/Player.Clothing.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate; 2 | 3 | public partial class Player 4 | { 5 | public ClothingContainer Clothing { get; protected set; } 6 | 7 | /// 8 | /// Set the clothes to whatever the player is wearing 9 | /// 10 | public void SetupClothing() 11 | { 12 | Clothing = new(); 13 | 14 | Clothing.ClearEntities(); 15 | Clothing.LoadFromClient( Client ); 16 | Clothing.DressEntity( this ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /code/Systems/Player/Player.GameEvents.cs: -------------------------------------------------------------------------------- 1 | using GameTemplate.Weapons; 2 | using Sandbox.Diagnostics; 3 | 4 | namespace GameTemplate; 5 | 6 | public partial class Player 7 | { 8 | static string realm = Game.IsServer ? "server" : "client"; 9 | static Logger eventLogger = new Logger( $"player/GameEvent/{realm}" ); 10 | 11 | public void RunGameEvent( string eventName ) 12 | { 13 | eventName = eventName.ToLowerInvariant(); 14 | 15 | Inventory.ActiveWeapon?.Components.GetAll() 16 | .ToList() 17 | .ForEach( x => x.OnGameEvent( eventName ) ); 18 | 19 | Controller.Mechanics.ToList() 20 | .ForEach( x => x.OnGameEvent( eventName ) ); 21 | 22 | eventLogger.Trace( $"OnGameEvent ({eventName})" ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /code/Systems/Player/Player.Input.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate; 2 | 3 | public partial class Player 4 | { 5 | /// 6 | /// Should be Input.AnalogMove 7 | /// 8 | [ClientInput] public Vector2 MoveInput { get; protected set; } 9 | 10 | /// 11 | /// Normalized accumulation of Input.AnalogLook 12 | /// 13 | [ClientInput] public Angles LookInput { get; protected set; } 14 | 15 | /// 16 | /// ? 17 | /// 18 | [ClientInput] public Entity ActiveWeaponInput { get; set; } 19 | 20 | /// 21 | /// Position a player should be looking from in world space. 22 | /// 23 | [Browsable( false )] 24 | public Vector3 EyePosition 25 | { 26 | get => Transform.PointToWorld( EyeLocalPosition ); 27 | set => EyeLocalPosition = Transform.PointToLocal( value ); 28 | } 29 | 30 | /// 31 | /// Position a player should be looking from in local to the entity coordinates. 32 | /// 33 | [Net, Predicted, Browsable( false )] 34 | public Vector3 EyeLocalPosition { get; set; } 35 | 36 | /// 37 | /// Rotation of the entity's "eyes", i.e. rotation for the camera when this entity is used as the view entity. 38 | /// 39 | [Browsable( false )] 40 | public Rotation EyeRotation 41 | { 42 | get => Transform.RotationToWorld( EyeLocalRotation ); 43 | set => EyeLocalRotation = Transform.RotationToLocal( value ); 44 | } 45 | 46 | /// 47 | /// 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. 48 | /// 49 | [Net, Predicted, Browsable( false )] 50 | public Rotation EyeLocalRotation { get; set; } 51 | 52 | /// 53 | /// Override the aim ray to use the player's eye position and rotation. 54 | /// 55 | public override Ray AimRay => new Ray( EyePosition, EyeRotation.Forward ); 56 | 57 | public override void BuildInput() 58 | { 59 | if ( Game.LocalClient.Components.Get() != null ) return; 60 | 61 | Inventory?.BuildInput(); 62 | 63 | MoveInput = Input.AnalogMove; 64 | var lookInput = (LookInput + Input.AnalogLook).Normal; 65 | 66 | // Since we're a FPS game, let's clamp the player's pitch between -90, and 90. 67 | LookInput = lookInput.WithPitch( lookInput.pitch.Clamp( -90f, 90f ) ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /code/Systems/Player/Player.Ragdoll.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate; 2 | 3 | public partial class Player 4 | { 5 | [ClientRpc] 6 | private void CreateRagdoll( Vector3 velocity, Vector3 forcePos, Vector3 force, int bone, bool bullet, bool blast ) 7 | { 8 | var ent = new ModelEntity(); 9 | ent.Tags.Add( "ragdoll", "solid", "debris" ); 10 | ent.Position = Position; 11 | ent.Rotation = Rotation; 12 | ent.Scale = Scale; 13 | ent.UsePhysicsCollision = true; 14 | ent.EnableAllCollisions = true; 15 | ent.SetModel( GetModelName() ); 16 | ent.CopyBonesFrom( this ); 17 | ent.CopyBodyGroups( this ); 18 | ent.CopyMaterialGroup( this ); 19 | ent.CopyMaterialOverrides( this ); 20 | ent.TakeDecalsFrom( this ); 21 | ent.EnableAllCollisions = true; 22 | ent.SurroundingBoundsMode = SurroundingBoundsType.Physics; 23 | ent.RenderColor = RenderColor; 24 | ent.PhysicsGroup.Velocity = velocity; 25 | ent.PhysicsEnabled = true; 26 | 27 | foreach ( var child in Children ) 28 | { 29 | if ( !child.Tags.Has( "clothes" ) ) continue; 30 | if ( child is not ModelEntity e ) continue; 31 | 32 | var model = e.GetModelName(); 33 | 34 | var clothing = new ModelEntity(); 35 | clothing.SetModel( model ); 36 | clothing.SetParent( ent, true ); 37 | clothing.RenderColor = e.RenderColor; 38 | clothing.CopyBodyGroups( e ); 39 | clothing.CopyMaterialGroup( e ); 40 | } 41 | 42 | if ( bullet ) 43 | { 44 | PhysicsBody body = bone > 0 ? ent.GetBonePhysicsBody( bone ) : null; 45 | 46 | if ( body != null ) 47 | { 48 | body.ApplyImpulseAt( forcePos, force * body.Mass ); 49 | } 50 | else 51 | { 52 | ent.PhysicsGroup.ApplyImpulse( force ); 53 | } 54 | } 55 | 56 | if ( blast ) 57 | { 58 | if ( ent.PhysicsGroup != null ) 59 | { 60 | ent.PhysicsGroup.AddVelocity( (Position - (forcePos + Vector3.Down * 100.0f)).Normal * (force.Length * 0.2f) ); 61 | var angularDir = (Rotation.FromYaw( 90 ) * force.WithZ( 0 ).Normal).Normal; 62 | ent.PhysicsGroup.AddAngularVelocity( angularDir * (force.Length * 0.02f) ); 63 | } 64 | } 65 | 66 | ent.DeleteAsync( 10.0f ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /code/Systems/Player/Player.cs: -------------------------------------------------------------------------------- 1 | using GameTemplate.Mechanics; 2 | using GameTemplate.Weapons; 3 | 4 | namespace GameTemplate; 5 | 6 | public partial class Player : AnimatedEntity 7 | { 8 | /// 9 | /// The controller is responsible for player movement and setting up EyePosition / EyeRotation. 10 | /// 11 | [BindComponent] public PlayerController Controller { get; } 12 | 13 | /// 14 | /// The animator is responsible for animating the player's current model. 15 | /// 16 | [BindComponent] public PlayerAnimator Animator { get; } 17 | 18 | /// 19 | /// The inventory is responsible for storing weapons for a player to use. 20 | /// 21 | [BindComponent] public Inventory Inventory { get; } 22 | 23 | /// 24 | /// The player's camera. 25 | /// 26 | [BindComponent] public PlayerCamera Camera { get; } 27 | 28 | /// 29 | /// Accessor for getting a player's active weapon. 30 | /// 31 | public Weapon ActiveWeapon => Inventory?.ActiveWeapon; 32 | 33 | /// 34 | /// The information for the last piece of damage this player took. 35 | /// 36 | public DamageInfo LastDamage { get; protected set; } 37 | 38 | /// 39 | /// How long since the player last played a footstep sound. 40 | /// 41 | public TimeSince TimeSinceFootstep { get; protected set; } = 0; 42 | 43 | /// 44 | /// The model your player will use. 45 | /// 46 | static Model PlayerModel = Model.Load( "models/citizen/citizen.vmdl" ); 47 | 48 | /// 49 | /// When the player is first created. This isn't called when a player respawns. 50 | /// 51 | public override void Spawn() 52 | { 53 | Model = PlayerModel; 54 | Predictable = true; 55 | 56 | // Default properties 57 | EnableDrawing = true; 58 | EnableHideInFirstPerson = true; 59 | EnableShadowInFirstPerson = true; 60 | EnableLagCompensation = true; 61 | EnableHitboxes = true; 62 | 63 | Tags.Add( "player" ); 64 | } 65 | 66 | /// 67 | /// Called when a player respawns, think of this as a soft spawn - we're only reinitializing transient data here. 68 | /// 69 | public void Respawn() 70 | { 71 | SetupPhysicsFromAABB( PhysicsMotionType.Keyframed, new Vector3( -16, -16, 0 ), new Vector3( 16, 16, 72 ) ); 72 | 73 | Health = 100; 74 | LifeState = LifeState.Alive; 75 | EnableAllCollisions = true; 76 | EnableDrawing = true; 77 | 78 | // Re-enable all children. 79 | Children.OfType() 80 | .ToList() 81 | .ForEach( x => x.EnableDrawing = true ); 82 | 83 | // We need a player controller to work with any kind of mechanics. 84 | Components.Create(); 85 | 86 | // Remove old mechanics. 87 | Components.RemoveAny(); 88 | 89 | // Add mechanics. 90 | Components.Create(); 91 | Components.Create(); 92 | Components.Create(); 93 | Components.Create(); 94 | Components.Create(); 95 | Components.Create(); 96 | 97 | Components.Create(); 98 | Components.Create(); 99 | 100 | var inventory = Components.Create(); 101 | inventory.AddWeapon( PrefabLibrary.Spawn( "prefabs/pistol.prefab" ) ); 102 | inventory.AddWeapon( PrefabLibrary.Spawn( "prefabs/smg.prefab" ), false ); 103 | 104 | SetupClothing(); 105 | 106 | GameManager.Current?.MoveToSpawnpoint( this ); 107 | ResetInterpolation(); 108 | } 109 | 110 | 111 | /// 112 | /// Called every server and client tick. 113 | /// 114 | /// 115 | public override void Simulate( IClient cl ) 116 | { 117 | Rotation = LookInput.WithPitch( 0f ).ToRotation(); 118 | 119 | Controller?.Simulate( cl ); 120 | Animator?.Simulate( cl ); 121 | Inventory?.Simulate( cl ); 122 | } 123 | 124 | /// 125 | /// Called every frame clientside. 126 | /// 127 | /// 128 | public override void FrameSimulate( IClient cl ) 129 | { 130 | Rotation = LookInput.WithPitch( 0f ).ToRotation(); 131 | 132 | Controller?.FrameSimulate( cl ); 133 | Camera?.Update( this ); 134 | } 135 | 136 | [ClientRpc] 137 | public void SetAudioEffect( string effectName, float strength, float velocity = 20f, float fadeOut = 4f ) 138 | { 139 | Audio.SetEffect( effectName, strength, velocity: 20.0f, fadeOut: 4.0f * strength ); 140 | } 141 | 142 | public override void TakeDamage( DamageInfo info ) 143 | { 144 | if ( LifeState != LifeState.Alive ) 145 | return; 146 | 147 | // Check for headshot damage 148 | var isHeadshot = info.Hitbox.HasTag( "head" ); 149 | if ( isHeadshot ) 150 | { 151 | info.Damage *= 2.5f; 152 | } 153 | 154 | // Check if we got hit by a bullet, if we did, play a sound. 155 | if ( info.HasTag( "bullet" ) ) 156 | { 157 | Sound.FromScreen( To.Single( Client ), "sounds/player/damage_taken_shot.sound" ); 158 | } 159 | 160 | // Play a deafening effect if we get hit by blast damage. 161 | if ( info.HasTag( "blast" ) ) 162 | { 163 | SetAudioEffect( To.Single( Client ), "flasthbang", info.Damage.LerpInverse( 0, 60 ) ); 164 | } 165 | 166 | if ( Health > 0 && info.Damage > 0 ) 167 | { 168 | Health -= info.Damage; 169 | 170 | if ( Health <= 0 ) 171 | { 172 | Health = 0; 173 | OnKilled(); 174 | } 175 | } 176 | 177 | this.ProceduralHitReaction( info ); 178 | } 179 | 180 | private async void AsyncRespawn() 181 | { 182 | await GameTask.DelaySeconds( 3f ); 183 | Respawn(); 184 | } 185 | 186 | public override void OnKilled() 187 | { 188 | if ( LifeState == LifeState.Alive ) 189 | { 190 | CreateRagdoll( Controller.Velocity, LastDamage.Position, LastDamage.Force, 191 | LastDamage.BoneIndex, LastDamage.HasTag( "bullet" ), LastDamage.HasTag( "blast" ) ); 192 | 193 | LifeState = LifeState.Dead; 194 | EnableAllCollisions = false; 195 | EnableDrawing = false; 196 | 197 | Controller.Remove(); 198 | Animator.Remove(); 199 | Inventory.Remove(); 200 | Camera.Remove(); 201 | 202 | // Disable all children as well. 203 | Children.OfType() 204 | .ToList() 205 | .ForEach( x => x.EnableDrawing = false ); 206 | 207 | AsyncRespawn(); 208 | } 209 | } 210 | 211 | /// 212 | /// Called clientside every time we fire the footstep anim event. 213 | /// 214 | public override void OnAnimEventFootstep( Vector3 pos, int foot, float volume ) 215 | { 216 | if ( !Game.IsClient ) 217 | return; 218 | 219 | if ( LifeState != LifeState.Alive ) 220 | return; 221 | 222 | if ( TimeSinceFootstep < 0.2f ) 223 | return; 224 | 225 | volume *= GetFootstepVolume(); 226 | 227 | TimeSinceFootstep = 0; 228 | 229 | var tr = Trace.Ray( pos, pos + Vector3.Down * 20 ) 230 | .Radius( 1 ) 231 | .Ignore( this ) 232 | .Run(); 233 | 234 | if ( !tr.Hit ) return; 235 | 236 | tr.Surface.DoFootstep( this, tr, foot, volume ); 237 | } 238 | 239 | protected float GetFootstepVolume() 240 | { 241 | return Controller.Velocity.WithZ( 0 ).Length.LerpInverse( 0.0f, 200.0f ) * 1f; 242 | } 243 | 244 | [ConCmd.Server( "kill" )] 245 | public static void DoSuicide() 246 | { 247 | (ConsoleSystem.Caller.Pawn as Player)?.TakeDamage( DamageInfo.Generic( 1000f ) ); 248 | } 249 | 250 | [ConCmd.Admin( "sethp" )] 251 | public static void SetHP( float value ) 252 | { 253 | (ConsoleSystem.Caller.Pawn as Player).Health = value; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /code/Systems/Player/PlayerAnimator.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate; 2 | 3 | public partial class PlayerAnimator : EntityComponent, ISingletonComponent 4 | { 5 | public virtual void Simulate( IClient cl ) 6 | { 7 | var player = Entity; 8 | var controller = player.Controller; 9 | CitizenAnimationHelper animHelper = new CitizenAnimationHelper( player ); 10 | 11 | animHelper.WithWishVelocity( controller.GetWishVelocity() ); 12 | animHelper.WithVelocity( controller.Velocity ); 13 | animHelper.WithLookAt( player.EyePosition + player.EyeRotation.Forward * 100.0f, 1.0f, 1.0f, 0.5f ); 14 | animHelper.AimAngle = player.EyeRotation; 15 | animHelper.FootShuffle = 0f; 16 | animHelper.DuckLevel = MathX.Lerp( animHelper.DuckLevel, 1 - controller.CurrentEyeHeight.Remap( 30, 72, 0, 1 ).Clamp( 0, 1 ), Time.Delta * 10.0f ); 17 | animHelper.VoiceLevel = (Game.IsClient && cl.IsValid()) ? cl.Voice.LastHeard < 0.5f ? cl.Voice.CurrentLevel : 0.0f : 0.0f; 18 | animHelper.IsGrounded = controller.GroundEntity != null; 19 | animHelper.IsSwimming = player.GetWaterLevel() >= 0.5f; 20 | animHelper.IsWeaponLowered = false; 21 | 22 | var weapon = player.ActiveWeapon; 23 | if ( weapon.IsValid() ) 24 | { 25 | player.SetAnimParameter( "holdtype", (int)weapon.HoldType ); 26 | player.SetAnimParameter( "holdtype_handedness", (int)weapon.Handedness ); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /code/Systems/Player/PlayerCamera.cs: -------------------------------------------------------------------------------- 1 | using Sandbox; 2 | 3 | namespace GameTemplate; 4 | 5 | public partial class PlayerCamera : EntityComponent, ISingletonComponent 6 | { 7 | public virtual void Update( Player player ) 8 | { 9 | Camera.Position = player.EyePosition; 10 | Camera.Rotation = player.EyeRotation; 11 | Camera.FieldOfView = Game.Preferences.FieldOfView; 12 | Camera.FirstPersonViewer = player; 13 | Camera.ZNear = 0.5f; 14 | 15 | // Post Processing 16 | var pp = Camera.Main.FindOrCreateHook(); 17 | pp.Sharpen = 0.05f; 18 | pp.Vignette.Intensity = 0.60f; 19 | pp.Vignette.Roundness = 1f; 20 | pp.Vignette.Smoothness = 0.3f; 21 | pp.Vignette.Color = Color.Black.WithAlpha( 1f ); 22 | pp.MotionBlur.Scale = 0f; 23 | pp.Saturation = 1f; 24 | pp.FilmGrain.Response = 1f; 25 | pp.FilmGrain.Intensity = 0.01f; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /code/Systems/Weapon/Components/PrimaryFireComponent.cs: -------------------------------------------------------------------------------- 1 | using GameTemplate.Mechanics; 2 | 3 | namespace GameTemplate.Weapons; 4 | 5 | [Prefab] 6 | public partial class PrimaryFire : WeaponComponent, ISingletonComponent 7 | { 8 | [Net, Prefab] public float BaseDamage { get; set; } 9 | [Net, Prefab] public float BulletRange { get; set; } 10 | [Net, Prefab] public int BulletCount { get; set; } 11 | [Net, Prefab] public float BulletForce { get; set; } 12 | [Net, Prefab] public float BulletSize { get; set; } 13 | [Net, Prefab] public float BulletSpread { get; set; } 14 | [Net, Prefab] public float FireDelay { get; set; } 15 | [Net, Prefab, ResourceType( "sound" )] public string FireSound { get; set; } 16 | 17 | TimeUntil TimeUntilCanFire { get; set; } 18 | 19 | protected override bool CanStart( Player player ) 20 | { 21 | if ( !Input.Down( "attack1") ) return false; 22 | if ( TimeUntilCanFire > 0 ) return false; 23 | 24 | return TimeSinceActivated > FireDelay; 25 | } 26 | 27 | public override void OnGameEvent( string eventName ) 28 | { 29 | if ( eventName == "sprint.stop" ) 30 | { 31 | TimeUntilCanFire = 0.2f; 32 | } 33 | } 34 | 35 | protected override void OnStart( Player player ) 36 | { 37 | base.OnStart( player ); 38 | 39 | player?.SetAnimParameter( "b_attack", true ); 40 | 41 | // Send clientside effects to the player. 42 | if ( Game.IsServer ) 43 | { 44 | player.PlaySound( FireSound ); 45 | DoShootEffects( To.Single( player ) ); 46 | } 47 | 48 | ShootBullet( BulletSpread, BulletForce, BulletSize, BulletCount, BulletRange ); 49 | } 50 | 51 | [ClientRpc] 52 | public static void DoShootEffects() 53 | { 54 | Game.AssertClient(); 55 | WeaponViewModel.Current?.SetAnimParameter( "fire", true ); 56 | } 57 | 58 | public IEnumerable TraceBullet( Vector3 start, Vector3 end, float radius ) 59 | { 60 | var tr = Trace.Ray( start, end ) 61 | .UseHitboxes() 62 | .WithAnyTags( "solid", "player", "glass" ) 63 | .Ignore( Entity ) 64 | .Size( radius ) 65 | .Run(); 66 | 67 | if ( tr.Hit ) 68 | { 69 | yield return tr; 70 | } 71 | } 72 | 73 | public void ShootBullet( float spread, float force, float bulletSize, int bulletCount = 1, float bulletRange = 5000f ) 74 | { 75 | // 76 | // Seed rand using the tick, so bullet cones match on client and server 77 | // 78 | Game.SetRandomSeed( Time.Tick ); 79 | 80 | for ( int i = 0; i < bulletCount; i++ ) 81 | { 82 | var rot = Rotation.LookAt( Player.AimRay.Forward ); 83 | 84 | var forward = rot.Forward; 85 | forward += (Vector3.Random + Vector3.Random + Vector3.Random + Vector3.Random) * spread * 0.25f; 86 | forward = forward.Normal; 87 | 88 | var damage = BaseDamage; 89 | 90 | foreach ( var tr in TraceBullet( Player.AimRay.Position, Player.AimRay.Position + forward * bulletRange, bulletSize ) ) 91 | { 92 | tr.Surface.DoBulletImpact( tr ); 93 | 94 | if ( !Game.IsServer ) continue; 95 | if ( !tr.Entity.IsValid() ) continue; 96 | 97 | var damageInfo = DamageInfo.FromBullet( tr.EndPosition, forward * 100 * force, damage ) 98 | .UsingTraceResult( tr ) 99 | .WithAttacker( Player ) 100 | .WithWeapon( Weapon ); 101 | 102 | tr.Entity.TakeDamage( damageInfo ); 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /code/Systems/Weapon/Components/ViewModelComponent.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate.Weapons; 2 | 3 | [Prefab] 4 | public partial class ViewModelComponent : WeaponComponent, ISingletonComponent 5 | { 6 | // I know that there's a metric fuck ton of Net properties here.. 7 | // ideally, when the prefab gets set up, we'd send the client a message with the prefab's name 8 | // so we can populate all the Prefab marked properties with their defaults. 9 | 10 | //// General 11 | [Net, Prefab, ResourceType( "vmdl" )] public string ViewModelPath { get; set; } 12 | 13 | [Net, Prefab] public float OverallWeight { get; set; } 14 | [Net, Prefab] public float WeightReturnForce { get; set; } 15 | [Net, Prefab] public float WeightDamping { get; set; } 16 | [Net, Prefab] public float AccelerationDamping { get; set; } 17 | [Net, Prefab] public float VelocityScale { get; set; } 18 | 19 | //// Walking & Bob 20 | [Net, Prefab] public Vector3 WalkCycleOffset { get; set; } 21 | [Net, Prefab] public Vector2 BobAmount { get; set; } 22 | 23 | //// Global 24 | [Net, Prefab] public Vector3 GlobalPositionOffset { get; set; } 25 | [Net, Prefab] public Angles GlobalAngleOffset { get; set; } 26 | 27 | //// Crouching 28 | [Net, Prefab] public Vector3 CrouchPositionOffset { get; set; } 29 | [Net, Prefab] public Angles CrouchAngleOffset { get; set; } 30 | } 31 | -------------------------------------------------------------------------------- /code/Systems/Weapon/Components/WeaponComponent.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate.Weapons; 2 | 3 | public partial class WeaponComponent : EntityComponent 4 | { 5 | protected Weapon Weapon => Entity; 6 | protected Player Player => Weapon.Owner as Player; 7 | protected string Identifier => DisplayInfo.ClassName.Trim(); 8 | protected virtual bool UseGameEvents => true; 9 | 10 | /// 11 | /// Is the weapon component active? Could mean are we shooting, reloading, aiming.. 12 | /// 13 | [Net, Predicted] public bool IsActive { get; protected set; } 14 | 15 | /// 16 | /// Time (in seconds) since IsActive = true 17 | /// 18 | [Net, Predicted] public TimeSince TimeSinceActivated { get; protected set; } 19 | 20 | DisplayInfo? displayInfo; 21 | 22 | /// 23 | /// Cached DisplayInfo for this weapon, so we don't fetch it every single time we fire events. 24 | /// 25 | public DisplayInfo DisplayInfo 26 | { 27 | get 28 | { 29 | displayInfo ??= DisplayInfo.For( this ); 30 | return displayInfo.Value; 31 | } 32 | } 33 | 34 | /// 35 | /// Accessor to grab components from the weapon 36 | /// 37 | /// 38 | /// 39 | public T GetComponent() where T : WeaponComponent 40 | { 41 | return Weapon.GetComponent(); 42 | } 43 | 44 | /// 45 | /// Run a weapon event 46 | /// 47 | /// 48 | public void RunGameEvent( string eventName ) 49 | { 50 | Player?.RunGameEvent( eventName ); 51 | } 52 | 53 | /// 54 | /// Called when the owning player has used this weapon. 55 | /// 56 | /// 57 | protected virtual void OnStart( Player player ) 58 | { 59 | TimeSinceActivated = 0; 60 | 61 | if ( UseGameEvents ) 62 | RunGameEvent( $"{Identifier}.start" ); 63 | } 64 | 65 | /// 66 | /// Dictates whether this entity is usable by given user. 67 | /// 68 | /// 69 | /// Return true if the given entity can use/interact with this entity. 70 | protected virtual bool CanStart( Player player ) 71 | { 72 | return true; 73 | } 74 | 75 | /// 76 | /// Called every tick. 77 | /// 78 | /// 79 | /// 80 | public virtual void Simulate( IClient cl, Player player ) 81 | { 82 | var before = IsActive; 83 | 84 | if ( !IsActive && CanStart( player ) ) 85 | { 86 | using ( Sandbox.Entity.LagCompensation() ) 87 | { 88 | OnStart( player ); 89 | IsActive = true; 90 | } 91 | } 92 | else if ( before && !CanStart( player ) ) 93 | { 94 | IsActive = false; 95 | OnStop( player ); 96 | } 97 | } 98 | 99 | /// 100 | /// Called when the component action stops. See 101 | /// 102 | /// 103 | protected virtual void OnStop( Player player ) 104 | { 105 | if ( UseGameEvents ) 106 | RunGameEvent( $"{Identifier}.stop" ); 107 | } 108 | 109 | /// 110 | /// Called when the weapon gets made on the server. 111 | /// NOTE: Need to remove this as we should just be able to use OnActivated 112 | /// 113 | /// 114 | public virtual void Initialize( Weapon weapon ) 115 | { 116 | // 117 | } 118 | 119 | /// 120 | /// Called when a game event is sent to the player. 121 | /// 122 | /// 123 | public virtual void OnGameEvent( string eventName ) 124 | { 125 | // 126 | } 127 | 128 | /// 129 | /// Called every Weapon.BuildInput 130 | /// 131 | public virtual void BuildInput() 132 | { 133 | // 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /code/Systems/Weapon/Weapon.Components.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate.Weapons; 2 | 3 | public partial class Weapon 4 | { 5 | public T GetComponent() where T : WeaponComponent 6 | { 7 | return Components.Get( false ); 8 | } 9 | 10 | protected void SimulateComponents( IClient cl ) 11 | { 12 | var player = Owner as Player; 13 | foreach ( var component in Components.GetAll() ) 14 | { 15 | component.Simulate( cl, player ); 16 | } 17 | } 18 | 19 | public void RunGameEvent( string eventName ) 20 | { 21 | Player?.RunGameEvent( eventName ); 22 | } 23 | 24 | public override void BuildInput() 25 | { 26 | foreach( var component in Components.GetAll() ) 27 | { 28 | component.BuildInput(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /code/Systems/Weapon/Weapon.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate.Weapons; 2 | 3 | [Prefab, Title( "Weapon" ), Icon( "track_changes" )] 4 | public partial class Weapon : AnimatedEntity 5 | { 6 | // Won't be Net eventually, when we serialize prefabs on client 7 | [Net, Prefab, Category( "Animation" )] public WeaponHoldType HoldType { get; set; } = WeaponHoldType.Pistol; 8 | [Net, Prefab, Category( "Animation" )] public WeaponHandedness Handedness { get; set; } = WeaponHandedness.Both; 9 | [Net, Prefab, Category( "Animation" )] public float HoldTypePose { get; set; } = 0; 10 | 11 | public AnimatedEntity EffectEntity => ViewModelEntity.IsValid() ? ViewModelEntity : this; 12 | public WeaponViewModel ViewModelEntity { get; protected set; } 13 | public Player Player => Owner as Player; 14 | 15 | public override void Spawn() 16 | { 17 | EnableHideInFirstPerson = true; 18 | EnableShadowInFirstPerson = true; 19 | EnableDrawing = false; 20 | } 21 | 22 | /// 23 | /// Can we holster the weapon right now? Reasons to reject this could be that we're reloading the weapon.. 24 | /// 25 | /// 26 | public bool CanHolster( Player player ) 27 | { 28 | return true; 29 | } 30 | 31 | /// 32 | /// Called when the weapon gets holstered. 33 | /// 34 | public void OnHolster( Player player ) 35 | { 36 | EnableDrawing = false; 37 | 38 | if ( Game.IsServer ) 39 | DestroyViewModel( To.Single( player ) ); 40 | } 41 | 42 | /// 43 | /// Can we deploy this weapon? Reasons to reject this could be that we're performing an action. 44 | /// 45 | /// 46 | public bool CanDeploy( Player player ) 47 | { 48 | return true; 49 | } 50 | 51 | /// 52 | /// Called when the weapon gets deployed. 53 | /// 54 | public void OnDeploy( Player player ) 55 | { 56 | SetParent( player, true ); 57 | Owner = player; 58 | 59 | EnableDrawing = true; 60 | 61 | if ( Game.IsServer ) 62 | CreateViewModel( To.Single( player ) ); 63 | } 64 | 65 | [ClientRpc] 66 | public void CreateViewModel() 67 | { 68 | if ( GetComponent() is not ViewModelComponent comp ) return; 69 | 70 | var vm = new WeaponViewModel( this ); 71 | vm.Model = Model.Load( comp.ViewModelPath ); 72 | ViewModelEntity = vm; 73 | } 74 | 75 | [ClientRpc] 76 | public void DestroyViewModel() 77 | { 78 | if ( ViewModelEntity.IsValid() ) 79 | { 80 | ViewModelEntity.Delete(); 81 | } 82 | } 83 | 84 | public override void Simulate( IClient cl ) 85 | { 86 | SimulateComponents( cl ); 87 | } 88 | 89 | protected override void OnDestroy() 90 | { 91 | ViewModelEntity?.Delete(); 92 | } 93 | 94 | public override string ToString() 95 | { 96 | return $"Weapon ({Name})"; 97 | } 98 | } 99 | 100 | /// 101 | /// Describes the holdtype of a weapon, which tells our animgraph which animations to use. 102 | /// 103 | public enum WeaponHoldType 104 | { 105 | None, 106 | Pistol, 107 | Rifle, 108 | Shotgun, 109 | Item, 110 | Fists, 111 | Swing 112 | } 113 | 114 | /// 115 | /// Describes the handedness of a weapon, which hand (or both) we hold the weapon in. 116 | /// 117 | public enum WeaponHandedness 118 | { 119 | Both, 120 | Right, 121 | Left 122 | } 123 | 124 | -------------------------------------------------------------------------------- /code/Systems/Weapon/WeaponViewModel.Effects.cs: -------------------------------------------------------------------------------- 1 | using GameTemplate.Mechanics; 2 | 3 | namespace GameTemplate.Weapons; 4 | 5 | public partial class WeaponViewModel 6 | { 7 | protected ViewModelComponent Data => Weapon.GetComponent(); 8 | 9 | // Fields 10 | Vector3 SmoothedVelocity; 11 | Vector3 velocity; 12 | Vector3 acceleration; 13 | float VelocityClamp => 20f; 14 | float walkBob = 0; 15 | float upDownOffset = 0; 16 | float sprintLerp = 0; 17 | float crouchLerp = 0; 18 | float airLerp = 0; 19 | float sideLerp = 0; 20 | 21 | protected float MouseDeltaLerpX; 22 | protected float MouseDeltaLerpY; 23 | 24 | Vector3 positionOffsetTarget = Vector3.Zero; 25 | Rotation rotationOffsetTarget = Rotation.Identity; 26 | 27 | Vector3 realPositionOffset; 28 | Rotation realRotationOffset; 29 | 30 | protected void ApplyPositionOffset( Vector3 offset, float delta ) 31 | { 32 | var left = Camera.Rotation.Left; 33 | var up = Camera.Rotation.Up; 34 | var forward = Camera.Rotation.Forward; 35 | 36 | positionOffsetTarget += forward * offset.x * delta; 37 | positionOffsetTarget += left * offset.y * delta; 38 | positionOffsetTarget += up * offset.z * delta; 39 | } 40 | 41 | private float WalkCycle( float speed, float power, bool abs = false ) 42 | { 43 | var sin = MathF.Sin( walkBob * speed ); 44 | var sign = Math.Sign( sin ); 45 | 46 | if ( abs ) 47 | { 48 | sign = 1; 49 | } 50 | 51 | return MathF.Pow( sin, power ) * sign; 52 | } 53 | 54 | private void LerpTowards( ref float value, float desired, float speed ) 55 | { 56 | var delta = (desired - value) * speed * Time.Delta; 57 | var deltaAbs = MathF.Min( MathF.Abs( delta ), MathF.Abs( desired - value ) ) * MathF.Sign( delta ); 58 | 59 | if ( MathF.Abs( desired - value ) < 0.001f ) 60 | { 61 | value = desired; 62 | 63 | return; 64 | } 65 | 66 | value += deltaAbs; 67 | } 68 | 69 | private void ApplyDamping( ref Vector3 value, float damping ) 70 | { 71 | var magnitude = value.Length; 72 | 73 | if ( magnitude != 0 ) 74 | { 75 | var drop = magnitude * damping * Time.Delta; 76 | value *= Math.Max( magnitude - drop, 0 ) / magnitude; 77 | } 78 | } 79 | 80 | public void AddEffects() 81 | { 82 | var player = Weapon.Player; 83 | var controller = player?.Controller; 84 | if ( controller == null ) 85 | return; 86 | 87 | SmoothedVelocity += (controller.Velocity - SmoothedVelocity) * 5f * Time.Delta; 88 | 89 | var isGrounded = controller.GroundEntity != null; 90 | var speed = controller.Velocity.Length.LerpInverse( 0, 750 ); 91 | var bobSpeed = SmoothedVelocity.Length.LerpInverse( -250, 700 ); 92 | var isSprinting = controller.IsMechanicActive(); 93 | var left = Camera.Rotation.Left; 94 | var up = Camera.Rotation.Up; 95 | var forward = Camera.Rotation.Forward; 96 | var isCrouching = controller.IsMechanicActive(); 97 | 98 | LerpTowards( ref sprintLerp, isSprinting ? 1 : 0, 10f ); 99 | LerpTowards( ref crouchLerp, isCrouching ? 1 : 0, 7f ); 100 | LerpTowards( ref airLerp, isGrounded ? 0 : 1, 10f ); 101 | 102 | var leftAmt = left.WithZ( 0 ).Normal.Dot( controller.Velocity.Normal ); 103 | LerpTowards( ref sideLerp, leftAmt, 5f ); 104 | 105 | bobSpeed += sprintLerp * 0.1f; 106 | 107 | if ( isGrounded ) 108 | { 109 | walkBob += Time.Delta * 30.0f * bobSpeed; 110 | } 111 | 112 | walkBob %= 360; 113 | 114 | var mouseDeltaX = -Input.MouseDelta.x * Time.Delta * Data.OverallWeight; 115 | var mouseDeltaY = -Input.MouseDelta.y * Time.Delta * Data.OverallWeight; 116 | 117 | acceleration += Vector3.Left * mouseDeltaX * -1f; 118 | acceleration += Vector3.Up * mouseDeltaY * -2f; 119 | acceleration += -velocity * Data.WeightReturnForce * Time.Delta; 120 | 121 | // Apply horizontal offsets based on walking direction 122 | var horizontalForwardBob = WalkCycle( 0.5f, 3f ) * speed * Data.WalkCycleOffset.x * Time.Delta; 123 | 124 | acceleration += forward.WithZ( 0 ).Normal.Dot( controller.Velocity.Normal ) * Vector3.Forward * Data.BobAmount.x * horizontalForwardBob; 125 | 126 | // Apply left bobbing and up/down bobbing 127 | acceleration += Vector3.Left * WalkCycle( 0.5f, 2f ) * speed * Data.WalkCycleOffset.y * (1 + sprintLerp) * Time.Delta; 128 | acceleration += Vector3.Up * WalkCycle( 0.5f, 2f, true ) * speed * Data.WalkCycleOffset.z * Time.Delta; 129 | acceleration += left.WithZ( 0 ).Normal.Dot( controller.Velocity.Normal ) * Vector3.Left * speed * Data.BobAmount.y * Time.Delta; 130 | 131 | velocity += acceleration * Time.Delta; 132 | 133 | ApplyDamping( ref acceleration, Data.AccelerationDamping ); 134 | ApplyDamping( ref velocity, Data.WeightDamping ); 135 | velocity = velocity.Normal * Math.Clamp( velocity.Length, 0, VelocityClamp ); 136 | 137 | Position = Camera.Position; 138 | Rotation = Camera.Rotation; 139 | 140 | positionOffsetTarget = Vector3.Zero; 141 | rotationOffsetTarget = Rotation.Identity; 142 | 143 | { 144 | // Global 145 | rotationOffsetTarget *= Rotation.From( Data.GlobalAngleOffset ); 146 | positionOffsetTarget += forward * (velocity.x * Data.VelocityScale + Data.GlobalPositionOffset.x); 147 | positionOffsetTarget += left * (velocity.y * Data.VelocityScale + Data.GlobalPositionOffset.y); 148 | positionOffsetTarget += up * (velocity.z * Data.VelocityScale + Data.GlobalPositionOffset.z + upDownOffset); 149 | 150 | float cycle = Time.Now * 10.0f; 151 | 152 | // Crouching 153 | rotationOffsetTarget *= Rotation.From( Data.CrouchAngleOffset * crouchLerp ); 154 | ApplyPositionOffset( Data.CrouchPositionOffset, crouchLerp ); 155 | 156 | // Air 157 | ApplyPositionOffset( new( 0, 0, 1 ), airLerp ); 158 | 159 | // Sprinting Camera Rotation 160 | Camera.Rotation *= Rotation.From( 161 | new Angles( 162 | MathF.Abs( MathF.Sin( cycle ) * 1.0f ), 163 | MathF.Cos( cycle ), 164 | 0 165 | ) * sprintLerp * 0.3f ); 166 | } 167 | 168 | realRotationOffset = rotationOffsetTarget; 169 | realPositionOffset = positionOffsetTarget; 170 | 171 | Rotation *= realRotationOffset; 172 | Position += realPositionOffset; 173 | 174 | Camera.Main.SetViewModelCamera( 85f, 1, 2048 ); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /code/Systems/Weapon/WeaponViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate.Weapons; 2 | 3 | [Title( "ViewModel" ), Icon( "pan_tool" )] 4 | public partial class WeaponViewModel : AnimatedEntity 5 | { 6 | /// 7 | /// All active view models. 8 | /// 9 | public static WeaponViewModel Current; 10 | 11 | protected Weapon Weapon { get; init; } 12 | 13 | public WeaponViewModel( Weapon weapon ) 14 | { 15 | if ( Current.IsValid() ) 16 | { 17 | Current.Delete(); 18 | } 19 | 20 | Current = this; 21 | EnableShadowCasting = false; 22 | EnableViewmodelRendering = true; 23 | Weapon = weapon; 24 | } 25 | 26 | protected override void OnDestroy() 27 | { 28 | Current = null; 29 | } 30 | 31 | [GameEvent.Client.PostCamera] 32 | public void PlaceViewmodel() 33 | { 34 | if ( Game.IsRunningInVR ) 35 | return; 36 | 37 | Camera.Main.SetViewModelCamera( 80f, 1, 500 ); 38 | AddEffects(); 39 | } 40 | 41 | public override Sound PlaySound( string soundName, string attachment ) 42 | { 43 | if ( Owner.IsValid() ) 44 | return Owner.PlaySound( soundName, attachment ); 45 | 46 | return base.PlaySound( soundName, attachment ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /code/UI/Chat.cs: -------------------------------------------------------------------------------- 1 | namespace GameTemplate.UI; 2 | 3 | public partial class Chat 4 | { 5 | [ConCmd.Client( "chat_add", CanBeCalledFromServer = true )] 6 | public static void AddChatEntry( string name, string message, string playerId = "0", bool isInfo = false ) 7 | { 8 | Current?.AddEntry( name, message, long.Parse( playerId ), isInfo ); 9 | 10 | // Only log clientside if we're not the listen server host 11 | if ( !Game.IsListenServer ) 12 | { 13 | Log.Info( $"{name}: {message}" ); 14 | } 15 | } 16 | 17 | public static void AddChatEntry( To target, string name, string message, long playerId = 0, bool isInfo = false ) 18 | { 19 | AddChatEntry( target, name, message, playerId.ToString(), isInfo ); 20 | } 21 | 22 | [ConCmd.Client( "chat_addinfo", CanBeCalledFromServer = true )] 23 | public static void AddInformation( string message ) 24 | { 25 | Current?.AddEntry( null, message ); 26 | } 27 | 28 | [ConCmd.Server( "chat_say" )] 29 | public static void Say( string message ) 30 | { 31 | // todo - reject more stuff 32 | if ( message.Contains( '\n' ) || message.Contains( '\r' ) ) 33 | return; 34 | 35 | Log.Info( $"{ConsoleSystem.Caller}: {message}" ); 36 | AddChatEntry( To.Everyone, ConsoleSystem.Caller.Name, message, ConsoleSystem.Caller.SteamId ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/UI/Chat.razor: -------------------------------------------------------------------------------- 1 | @namespace GameTemplate.UI 2 | 3 | 4 |
5 | 6 | 7 | 8 | @code 9 | { 10 | public static Chat Current; 11 | 12 | public Panel Canvas { get; protected set; } 13 | public TextEntry Input { get; protected set; } 14 | 15 | Queue Rows = new(); 16 | 17 | protected int MaxItems => 100; 18 | protected float MessageLifetime => 10f; 19 | 20 | public bool IsOpen 21 | { 22 | get => HasClass( "open" ); 23 | set 24 | { 25 | SetClass( "open", value ); 26 | if ( value ) 27 | { 28 | Input.Focus(); 29 | Input.Text = string.Empty; 30 | Input.Label.SetCaretPosition( 0 ); 31 | } 32 | } 33 | } 34 | 35 | protected override void OnAfterTreeRender( bool firstTime ) 36 | { 37 | base.OnAfterTreeRender( firstTime ); 38 | 39 | Canvas.PreferScrollToBottom = true; 40 | Input.AcceptsFocus = true; 41 | Input.AllowEmojiReplace = true; 42 | 43 | Current = this; 44 | } 45 | 46 | public override void Tick() 47 | { 48 | if ( Sandbox.Input.Pressed( "chat" ) ) 49 | Open(); 50 | 51 | Input.Placeholder = string.IsNullOrEmpty( Input.Text ) ? "Enter your message..." : string.Empty; 52 | } 53 | 54 | void Open() 55 | { 56 | AddClass( "open" ); 57 | Input.Focus(); 58 | Canvas.TryScrollToBottom(); 59 | } 60 | 61 | void Close() 62 | { 63 | RemoveClass( "open" ); 64 | Input.Blur(); 65 | } 66 | 67 | void Submit() 68 | { 69 | Close(); 70 | 71 | var msg = Input.Text.Trim(); 72 | Input.Text = ""; 73 | 74 | if ( string.IsNullOrWhiteSpace( msg ) ) 75 | return; 76 | 77 | Say( msg ); 78 | } 79 | 80 | public void AddEntry( string name, string message, long playerId = 0, bool isInfo = false ) 81 | { 82 | var e = Canvas.AddChild(); 83 | 84 | var player = Game.LocalPawn; 85 | if ( !player.IsValid() ) return; 86 | 87 | if ( playerId > 0 ) 88 | e.PlayerId = playerId; 89 | 90 | e.Message = message; 91 | e.Name = $"{name}"; 92 | 93 | e.SetClass( "noname", string.IsNullOrEmpty( name ) ); 94 | e.SetClass( "info", isInfo ); 95 | e.BindClass( "stale", () => e.Lifetime > MessageLifetime ); 96 | 97 | var cl = Game.Clients.ToList().FirstOrDefault( x => x.SteamId == playerId ); 98 | if ( cl.IsValid() ) 99 | e.SetClass( "friend", cl.IsFriend || Game.SteamId == playerId ); 100 | 101 | Canvas.TryScrollToBottom(); 102 | 103 | Rows.Enqueue( e ); 104 | 105 | // Kill an item if we need to 106 | if ( Rows.Count > MaxItems ) 107 | Rows.Dequeue().Delete(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /code/UI/ChatRow.razor: -------------------------------------------------------------------------------- 1 | @namespace GameTemplate.UI 2 | 3 | 4 | @if ( PlayerId > 0 ) 5 | { 6 |
7 | 8 |
9 | } 10 |
11 | 12 |
13 | 14 |
15 | 16 |
17 |
18 | 19 | @code 20 | { 21 | public string Name { get; set; } 22 | public string Message { get; set; } 23 | public long PlayerId { get; set; } 24 | public TimeSince Lifetime { get; init; } = 0; 25 | } 26 | -------------------------------------------------------------------------------- /code/UI/Crosshair.razor: -------------------------------------------------------------------------------- 1 | @using GameTemplate.Weapons; 2 | 3 | @namespace GameTemplate 4 | @inherits Panel 5 | 6 | 58 | 59 | 60 |
61 |
62 |
63 |
64 |
65 |
66 | 67 | @code { 68 | public float PixelDistance { get; set; } = 20f; 69 | public string Distance => $"{PixelDistance}px"; 70 | 71 | protected override void OnAfterTreeRender(bool firstTime) 72 | { 73 | base.OnAfterTreeRender( firstTime ); 74 | } 75 | 76 | protected override int BuildHash() 77 | { 78 | return HashCode.Combine( Time.Now ); 79 | } 80 | 81 | public override void Tick() 82 | { 83 | var player = Game.LocalPawn as Player; 84 | var weapon = player?.Inventory?.ActiveWeapon; 85 | var isAiming = weapon?.Tags.Has( "aiming" ) ?? false; 86 | 87 | var distance = player?.Velocity.Length.LerpInverse( 0, 750, true ).Remap( 0, 1, 20, 60 ) ?? 20f; 88 | float timeSinceActivated = weapon?.GetComponent()?.TimeSinceActivated ?? 1; 89 | var fireAmt = 1 - timeSinceActivated.LerpInverse( 0, 0.25f, true ); 90 | 91 | distance += fireAmt * 50; 92 | 93 | PixelDistance = distance; 94 | 95 | SetClass( "visible", !isAiming ); 96 | } 97 | } -------------------------------------------------------------------------------- /code/UI/Hud.razor: -------------------------------------------------------------------------------- 1 | @namespace GameTemplate.UI 2 | @inherits RootPanel 3 | @attribute [StyleSheet( "/UI/StyleSheets/_hud.scss" )] 4 | 5 | @if ( IsDevCamera ) return; 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | @code 15 | { 16 | public bool IsDevCamera => Game.LocalClient.Components.Get() != null; 17 | 18 | protected override int BuildHash() 19 | { 20 | return HashCode.Combine( IsDevCamera ); 21 | } 22 | } -------------------------------------------------------------------------------- /code/UI/Info.razor: -------------------------------------------------------------------------------- 1 | @using GameTemplate.Weapons; 2 | 3 | @namespace GameTemplate 4 | 5 | 6 | 7 | 8 | monitor_heart 9 | 10 | 11 | @{ 12 | if ( Inventory == null ) return; 13 | int i = 0; 14 | foreach( var weapon in Inventory.Weapons ) 15 | { 16 | i++; 17 | 18 | 19 | 20 | 21 | } 22 | } 23 | 24 | 25 | @code 26 | { 27 | public string Health => $"{Player.Health:F0}"; 28 | public Player Player => Game.LocalPawn as Player; 29 | public Weapon Weapon => Player?.ActiveWeapon; 30 | public Inventory Inventory => Player.Inventory; 31 | 32 | protected override int BuildHash() 33 | { 34 | return HashCode.Combine( Player?.Health, Weapon ); 35 | } 36 | } -------------------------------------------------------------------------------- /code/UI/Players.razor: -------------------------------------------------------------------------------- 1 | @namespace GameTemplate.UI 2 | @inherits Panel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | @foreach( var cl in Game.Clients ) 12 | { 13 | 14 | 15 | 16 | 17 | 18 | } 19 | 20 | 21 | 22 | @code 23 | { 24 | protected override int BuildHash() 25 | { 26 | return HashCode.Combine( Game.Clients.Count ); 27 | } 28 | 29 | public override void Tick() 30 | { 31 | SetClass( "open", Input.Down( "score" ) ); 32 | } 33 | } -------------------------------------------------------------------------------- /code/UI/StyleSheets/_chat.scss: -------------------------------------------------------------------------------- 1 | Chat 2 | { 3 | position: absolute; 4 | bottom: -16px; 5 | left: 32px; 6 | z-index: 1001; 7 | flex-direction: column; 8 | width: 505px; 9 | max-height: 435px; 10 | font-size: 16px; 11 | pointer-events: none; 12 | transition: all 0.1s ease, border 0s ease; 13 | padding: 18px; 14 | 15 | textentry 16 | { 17 | transition: opacity 0.1s ease; 18 | flex-shrink: 0; 19 | min-height: 44px; 20 | opacity: 0; 21 | background-color: rgba( #090d10, 0.9 ); 22 | border-radius: $rounding; 23 | margin-bottom: 28px; 24 | } 25 | 26 | .placeholder 27 | { 28 | color: #c0cee7; 29 | } 30 | 31 | .content-label 32 | { 33 | margin-left: 16px; 34 | } 35 | 36 | .placeholder, .content-label 37 | { 38 | color: darken( #c0cee7, 20% ); 39 | font-size: 14px; 40 | font-weight: 400; 41 | text-shadow: 2px 2px 1px rgba( black, 0.1 ); 42 | margin-top: 13px; 43 | } 44 | 45 | .canvas 46 | { 47 | flex-direction: column; 48 | align-items: flex-start; 49 | overflow: scroll; 50 | 51 | ChatRow 52 | { 53 | max-width: 100%; 54 | color: white; 55 | opacity: 1; 56 | flex-shrink: 0; 57 | background-color: rgba( #090d10, 0.9 ); 58 | padding: 4px 10px; 59 | border-radius: $rounding; 60 | transition: opacity 0.15s ease; 61 | margin-bottom: 6px; 62 | 63 | .header 64 | { 65 | flex-shrink: 0; 66 | flex-grow: 0; 67 | padding-left: 4px; 68 | margin-top: 5px; 69 | font-weight: 700; 70 | font-size: 16px; 71 | color: $blue; 72 | padding-right: 8px; 73 | text-shadow: 2px 2px 1px rgba( black, 0.3 ); 74 | } 75 | 76 | .msg 77 | { 78 | margin-top: 6px; 79 | color: #c0cee7; 80 | font-size: 16px; 81 | text-shadow: 2px 2px 1px rgba( black, 0.1 ); 82 | } 83 | 84 | &.info 85 | { 86 | .header, .msg 87 | { 88 | color: darken( #c0cee7, 20% ); 89 | } 90 | } 91 | 92 | img 93 | { 94 | flex-shrink: 0; 95 | height: 30px; 96 | width: 30px; 97 | border-radius: 100px; 98 | } 99 | 100 | &.noname 101 | { 102 | .name 103 | { 104 | display: none; 105 | } 106 | } 107 | 108 | &.is-lobby, &.friend 109 | { 110 | .name 111 | { 112 | color: #849a74; 113 | } 114 | } 115 | 116 | &.stale 117 | { 118 | transition: opacity 1s ease; 119 | opacity: 0; 120 | } 121 | } 122 | } 123 | 124 | &.open 125 | { 126 | pointer-events: all; 127 | backdrop-filter-blur: 20px; 128 | background-image: linear-gradient( to top, rgba( #1b1b35, 0.9 ), rgba( #1c1c38, 0.85 ), rgba( #1c1c38, 0.8 ) 80%, rgba( #1c1c38, 0.5 ) ); 129 | bottom: 0; 130 | border-radius: $rounding $rounding 0 0; 131 | 132 | textentry 133 | { 134 | opacity: 1; 135 | width: 100%; 136 | cursor: text; 137 | } 138 | 139 | .canvas 140 | { 141 | ChatRow 142 | { 143 | transition: opacity 0.1s ease; 144 | opacity: 1; 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /code/UI/StyleSheets/_hud.scss: -------------------------------------------------------------------------------- 1 | $blue: #3273eb; 2 | $rounding: 4px; 3 | 4 | @import "_chat.scss"; 5 | @import "_info.scss"; 6 | @import "_players.scss"; 7 | 8 | .with-flex 9 | { 10 | &.column 11 | { 12 | flex-direction: column; 13 | } 14 | 15 | &.column-reverse, .column-reversed 16 | { 17 | flex-direction: column-reverse; 18 | } 19 | 20 | &.row 21 | { 22 | flex-direction: row; 23 | } 24 | 25 | &.wrap 26 | { 27 | flex-wrap: wrap; 28 | } 29 | 30 | &.row-reverse, row-reversed 31 | { 32 | flex-direction: row-reverse; 33 | } 34 | 35 | cell 36 | { 37 | border-radius: $rounding; 38 | background-color: rgba( black, 0.7 ); 39 | } 40 | 41 | i 42 | { 43 | font-family: Material Icons Outlined; 44 | } 45 | } 46 | 47 | .with-justify-end 48 | { 49 | justify-content: flex-end; 50 | } 51 | 52 | .with-header 53 | { 54 | font-size: 24px; 55 | color: white; 56 | font-weight: 600; 57 | } 58 | 59 | .with-subheader 60 | { 61 | font-size: 18px; 62 | color: lightgrey; 63 | font-weight: 500; 64 | } 65 | 66 | .with-text 67 | { 68 | font-size: 16px; 69 | color: white; 70 | font-weight: 400; 71 | } 72 | 73 | .with-description 74 | { 75 | font-size: 13px; 76 | color: white; 77 | font-weight: 400; 78 | } 79 | 80 | .with-shadow 81 | { 82 | text-shadow: 0px 0px 2px rgba( black, 0.3 ); 83 | } 84 | 85 | .with-icon 86 | { 87 | justify-content: center; 88 | align-items: center; 89 | align-content: center; 90 | } 91 | 92 | .with-avatar 93 | { 94 | margin-left: 2px; 95 | border-radius: 100px; 96 | width: 32px; 97 | height: 32px; 98 | } 99 | 100 | .with-gap 101 | { 102 | gap: 8px; 103 | } 104 | 105 | .with-gap-small 106 | { 107 | gap: 4px; 108 | } 109 | 110 | .with-padding 111 | { 112 | padding: 4px; 113 | } 114 | 115 | .with-padding-large 116 | { 117 | padding: 8px; 118 | } 119 | 120 | .with-center 121 | { 122 | align-items: center; 123 | justify-content: center; 124 | } -------------------------------------------------------------------------------- /code/UI/StyleSheets/_info.scss: -------------------------------------------------------------------------------- 1 | Info 2 | { 3 | position: absolute; 4 | bottom: 64px; 5 | right: 64px; 6 | width: 150px; 7 | flex-direction: column-reverse; 8 | 9 | cell 10 | { 11 | transition: all 0.2s ease; 12 | 13 | &.active 14 | { 15 | padding: 16px; 16 | 17 | .name 18 | { 19 | color: white; 20 | font-size: 24px; 21 | } 22 | } 23 | 24 | .id 25 | { 26 | font-size: 12px; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /code/UI/StyleSheets/_players.scss: -------------------------------------------------------------------------------- 1 | Players 2 | { 3 | position: absolute; 4 | left: 32px; 5 | top: 32px; 6 | 7 | transition: all 0.2s ease; 8 | transform: scale( 0.95 ); 9 | transform-origin: top left; 10 | 11 | opacity: 0; 12 | 13 | &.open 14 | { 15 | transform: scale( 1 ); 16 | opacity: 1; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /prefabs/pistol.prefab: -------------------------------------------------------------------------------- 1 | { 2 | "SchemaVersion": 1, 3 | "Name": null, 4 | "Description": null, 5 | "Root": { 6 | "Guid": "60f4b990-4b49-4a59-aec6-45558e69826c", 7 | "Class": "Weapon", 8 | "Entities": [], 9 | "Components": [ 10 | { 11 | "Guid": "55e6a030-4330-4721-87c5-6364a351dab8", 12 | "Class": "PrimaryFire", 13 | "Keys": { 14 | "BulletCount": { 15 | "Type": "Int32", 16 | "Value": "1" 17 | }, 18 | "BulletForce": { 19 | "Type": "Single", 20 | "Value": "50" 21 | }, 22 | "BulletSize": { 23 | "Type": "Single", 24 | "Value": "2" 25 | }, 26 | "BulletSpread": { 27 | "Type": "Single", 28 | "Value": "0.05" 29 | }, 30 | "FireDelay": { 31 | "Type": "Single", 32 | "Value": "0.25" 33 | }, 34 | "BaseDamage": { 35 | "Type": "Single", 36 | "Value": "25" 37 | }, 38 | "BulletRange": { 39 | "Type": "Single", 40 | "Value": "1024" 41 | }, 42 | "FireSound": { 43 | "Type": "String", 44 | "Value": "\u0022weapons/rust_pistol/sound/rust_pistol.shoot.sound\u0022" 45 | }, 46 | "_name": { 47 | "Value": "\u0022Pistol Shoot\u0022" 48 | }, 49 | "_tags": { 50 | "Value": "[]" 51 | } 52 | } 53 | }, 54 | { 55 | "Guid": "89f43426-e4ff-4080-8d5d-1747fe2f56b9", 56 | "Class": "ViewModelComponent", 57 | "Keys": { 58 | "OverallWeight": { 59 | "Type": "Single", 60 | "Value": "1" 61 | }, 62 | "WeightReturnForce": { 63 | "Type": "Single", 64 | "Value": "100" 65 | }, 66 | "WeightDamping": { 67 | "Type": "Single", 68 | "Value": "10" 69 | }, 70 | "AccelerationDamping": { 71 | "Type": "Single", 72 | "Value": "1" 73 | }, 74 | "VelocityScale": { 75 | "Type": "Single", 76 | "Value": "0.5" 77 | }, 78 | "RotationalPivotForce": { 79 | "Type": "Single", 80 | "Value": "1" 81 | }, 82 | "RotationalScale": { 83 | "Type": "Single", 84 | "Value": "15" 85 | }, 86 | "WalkCycleOffset": { 87 | "Type": "Vector3", 88 | "Value": "\u00220,0,0\u0022" 89 | }, 90 | "BobAmount": { 91 | "Type": "Vector2", 92 | "Value": "\u00220,0\u0022" 93 | }, 94 | "GlobalLerpPower": { 95 | "Type": "Single", 96 | "Value": "0" 97 | }, 98 | "GlobalPositionOffset": { 99 | "Type": "Vector3", 100 | "Value": "\u0022-10.5,7.05,-1.95\u0022" 101 | }, 102 | "GlobalAngleOffset": { 103 | "Type": "Angles", 104 | "Value": "\u00220,0,0\u0022" 105 | }, 106 | "CrouchPositionOffset": { 107 | "Type": "Vector3", 108 | "Value": "\u00220,0,0\u0022" 109 | }, 110 | "CrouchAngleOffset": { 111 | "Type": "Angles", 112 | "Value": "\u00220,0,0\u0022" 113 | }, 114 | "AvoidancePositionOffset": { 115 | "Type": "Vector3", 116 | "Value": "\u00220,0,0\u0022" 117 | }, 118 | "AvoidanceAngleOffset": { 119 | "Type": "Angles", 120 | "Value": "\u00220,0,0\u0022" 121 | }, 122 | "SprintPositionOffset": { 123 | "Type": "Vector3", 124 | "Value": "\u00220,0,0\u0022" 125 | }, 126 | "AimAngleOffset": { 127 | "Type": "Angles", 128 | "Value": "\u00220,0,0\u0022" 129 | }, 130 | "AimFovOffset": { 131 | "Type": "Single", 132 | "Value": "0" 133 | }, 134 | "AimPositionOffset": { 135 | "Type": "Vector3", 136 | "Value": "\u00220,0,0\u0022" 137 | }, 138 | "SprintAngleOffset": { 139 | "Type": "Angles", 140 | "Value": "\u00220,0,0\u0022" 141 | }, 142 | "ViewModelPath": { 143 | "Type": "String", 144 | "Value": "\u0022weapons/rust_pistol/v_rust_pistol.vmdl\u0022" 145 | }, 146 | "_name": { 147 | "Value": "\u0022Pistol View Model\u0022" 148 | }, 149 | "_tags": { 150 | "Value": "[]" 151 | } 152 | } 153 | } 154 | ], 155 | "Keys": { 156 | "HoldTypePose": { 157 | "Type": "Single", 158 | "Value": "0" 159 | }, 160 | "Handedness": { 161 | "Type": "WeaponHandedness", 162 | "Value": "\u0022Both\u0022" 163 | }, 164 | "ViewModelPath": { 165 | "Type": "String", 166 | "Value": "\u0022weapons/rust_pistol/v_rust_pistol.vmdl\u0022" 167 | }, 168 | "HoldType": { 169 | "Type": "WeaponHoldType", 170 | "Value": "\u0022Pistol\u0022" 171 | }, 172 | "_name": { 173 | "Value": "\u0022Pistol\u0022" 174 | }, 175 | "_localposition": { 176 | "Value": "\u00220,0,0\u0022" 177 | }, 178 | "_localrotation": { 179 | "Value": "\u00220,0,0\u0022" 180 | }, 181 | "_scale": { 182 | "Value": "1" 183 | }, 184 | "_tags": { 185 | "Value": "\u0022\u0022" 186 | }, 187 | "Model": { 188 | "Type": "Model", 189 | "Value": "\u0022weapons/rust_pistol/rust_pistol.vmdl\u0022" 190 | } 191 | } 192 | } 193 | } -------------------------------------------------------------------------------- /prefabs/pistol.prefab_c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevulTj/sbox-fps_sample/452f7655903d0558ed9a884eb2b29c657fb297cc/prefabs/pistol.prefab_c -------------------------------------------------------------------------------- /prefabs/smg.prefab: -------------------------------------------------------------------------------- 1 | { 2 | "SchemaVersion": 1, 3 | "Name": null, 4 | "Description": null, 5 | "Root": { 6 | "Guid": "60f4b990-4b49-4a59-aec6-45558e69826c", 7 | "Class": "Weapon", 8 | "Entities": [], 9 | "Components": [ 10 | { 11 | "Guid": "55e6a030-4330-4721-87c5-6364a351dab8", 12 | "Class": "PrimaryFire", 13 | "Keys": { 14 | "BulletCount": { 15 | "Type": "Int32", 16 | "Value": "1" 17 | }, 18 | "BulletForce": { 19 | "Type": "Single", 20 | "Value": "50" 21 | }, 22 | "BulletSize": { 23 | "Type": "Single", 24 | "Value": "2" 25 | }, 26 | "BulletSpread": { 27 | "Type": "Single", 28 | "Value": "0.05" 29 | }, 30 | "FireDelay": { 31 | "Type": "Single", 32 | "Value": "0.1" 33 | }, 34 | "BaseDamage": { 35 | "Type": "Single", 36 | "Value": "12" 37 | }, 38 | "BulletRange": { 39 | "Type": "Single", 40 | "Value": "1024" 41 | }, 42 | "FireSound": { 43 | "Type": "String", 44 | "Value": "\u0022weapons/rust_smg/sounds/rust_smg.shoot.sound\u0022" 45 | }, 46 | "_name": { 47 | "Value": "\u0022SMG Shoot\u0022" 48 | }, 49 | "_tags": { 50 | "Value": "[]" 51 | } 52 | } 53 | }, 54 | { 55 | "Guid": "89f43426-e4ff-4080-8d5d-1747fe2f56b9", 56 | "Class": "ViewModelComponent", 57 | "Keys": { 58 | "OverallWeight": { 59 | "Type": "Single", 60 | "Value": "1" 61 | }, 62 | "WeightReturnForce": { 63 | "Type": "Single", 64 | "Value": "100" 65 | }, 66 | "WeightDamping": { 67 | "Type": "Single", 68 | "Value": "10" 69 | }, 70 | "AccelerationDamping": { 71 | "Type": "Single", 72 | "Value": "1" 73 | }, 74 | "VelocityScale": { 75 | "Type": "Single", 76 | "Value": "0.5" 77 | }, 78 | "RotationalPivotForce": { 79 | "Type": "Single", 80 | "Value": "1" 81 | }, 82 | "RotationalScale": { 83 | "Type": "Single", 84 | "Value": "15" 85 | }, 86 | "WalkCycleOffset": { 87 | "Type": "Vector3", 88 | "Value": "\u00220,0,0\u0022" 89 | }, 90 | "BobAmount": { 91 | "Type": "Vector2", 92 | "Value": "\u00220,0\u0022" 93 | }, 94 | "GlobalLerpPower": { 95 | "Type": "Single", 96 | "Value": "0" 97 | }, 98 | "GlobalPositionOffset": { 99 | "Type": "Vector3", 100 | "Value": "\u0022-10.5,7.05,-1.95\u0022" 101 | }, 102 | "GlobalAngleOffset": { 103 | "Type": "Angles", 104 | "Value": "\u00220,0,0\u0022" 105 | }, 106 | "CrouchPositionOffset": { 107 | "Type": "Vector3", 108 | "Value": "\u00220,0,0\u0022" 109 | }, 110 | "CrouchAngleOffset": { 111 | "Type": "Angles", 112 | "Value": "\u00220,0,0\u0022" 113 | }, 114 | "AvoidancePositionOffset": { 115 | "Type": "Vector3", 116 | "Value": "\u00220,0,0\u0022" 117 | }, 118 | "AvoidanceAngleOffset": { 119 | "Type": "Angles", 120 | "Value": "\u00220,0,0\u0022" 121 | }, 122 | "SprintPositionOffset": { 123 | "Type": "Vector3", 124 | "Value": "\u00220,0,0\u0022" 125 | }, 126 | "AimAngleOffset": { 127 | "Type": "Angles", 128 | "Value": "\u00220,0,0\u0022" 129 | }, 130 | "AimFovOffset": { 131 | "Type": "Single", 132 | "Value": "0" 133 | }, 134 | "AimPositionOffset": { 135 | "Type": "Vector3", 136 | "Value": "\u00220,0,0\u0022" 137 | }, 138 | "SprintAngleOffset": { 139 | "Type": "Angles", 140 | "Value": "\u00220,0,0\u0022" 141 | }, 142 | "ViewModelPath": { 143 | "Type": "String", 144 | "Value": "\u0022weapons/rust_smg/v_rust_smg.vmdl\u0022" 145 | }, 146 | "_name": { 147 | "Value": "\u0022SMG View Model\u0022" 148 | }, 149 | "_tags": { 150 | "Value": "[]" 151 | } 152 | } 153 | } 154 | ], 155 | "Keys": { 156 | "HoldTypePose": { 157 | "Type": "Single", 158 | "Value": "0" 159 | }, 160 | "Handedness": { 161 | "Type": "WeaponHandedness", 162 | "Value": "\u0022Left\u0022" 163 | }, 164 | "ViewModelPath": { 165 | "Type": "String", 166 | "Value": "\u0022weapons/rust_pistol/v_rust_pistol.vmdl\u0022" 167 | }, 168 | "HoldType": { 169 | "Type": "WeaponHoldType", 170 | "Value": "\u0022Rifle\u0022" 171 | }, 172 | "_name": { 173 | "Value": "\u0022SMG\u0022" 174 | }, 175 | "_localposition": { 176 | "Value": "\u00220,0,0\u0022" 177 | }, 178 | "_localrotation": { 179 | "Value": "\u00220,0,0\u0022" 180 | }, 181 | "_scale": { 182 | "Value": "1" 183 | }, 184 | "_tags": { 185 | "Value": "\u0022\u0022" 186 | }, 187 | "Model": { 188 | "Type": "Model", 189 | "Value": "\u0022weapons/rust_smg/rust_smg.vmdl\u0022" 190 | } 191 | } 192 | } 193 | } -------------------------------------------------------------------------------- /prefabs/smg.prefab_c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevulTj/sbox-fps_sample/452f7655903d0558ed9a884eb2b29c657fb297cc/prefabs/smg.prefab_c --------------------------------------------------------------------------------