├── .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 | 
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 |
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 |
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
--------------------------------------------------------------------------------