diff --git a/code/Entities/Lucker.cs b/code/Entities/Lucker.cs
index 3394cd4..92494b8 100644
--- a/code/Entities/Lucker.cs
+++ b/code/Entities/Lucker.cs
@@ -13,6 +13,7 @@ public partial class Lucker : Entity
///
/// The entity this Player currently controls
///
+ [Net]
public Entity Pawn { get; set; }
///
@@ -70,18 +71,20 @@ public partial class Lucker : Entity
public override void Simulate( IClient cl )
{
base.Simulate( cl );
-
+ Pawn?.Simulate(cl);
}
public override void FrameSimulate( IClient cl )
{
base.FrameSimulate( cl );
Camera?.Update();
+ Pawn?.FrameSimulate(cl);
}
public override void BuildInput()
{
base.BuildInput();
Camera?.BuildInput();
+ Pawn?.BuildInput();
}
}
diff --git a/code/Entities/MinigameManager.cs b/code/Entities/MinigameManager.cs
index 0832c8b..32a6a3f 100644
--- a/code/Entities/MinigameManager.cs
+++ b/code/Entities/MinigameManager.cs
@@ -29,7 +29,8 @@ public partial class MinigameManager : Entity
return;
}
- LoadedMinigame = AvailableMinigames.OrderBy( _ => Guid.NewGuid() ).FirstOrDefault();
+ // LoadedMinigame = AvailableMinigames.OrderBy( _ => Guid.NewGuid() ).FirstOrDefault();
+ LoadedMinigame = AvailableMinigames.Find( minigame => minigame.Name == "FPS Test" );
ChatBox.AddInformation( To.Everyone, $"Starting {LoadedMinigame.Name}" );
LoadedMinigame.Initialize( players );
}
diff --git a/code/EntityComponents/Lucker/Cameras/AbstractCamera.cs b/code/EntityComponents/Lucker/Cameras/AbstractCamera.cs
index 0d9e6d7..08645c5 100644
--- a/code/EntityComponents/Lucker/Cameras/AbstractCamera.cs
+++ b/code/EntityComponents/Lucker/Cameras/AbstractCamera.cs
@@ -6,11 +6,11 @@ namespace LuckerGame.Components.Lucker.Cameras;
public abstract class AbstractCamera : EntityComponent, ISingletonComponent
{
public virtual bool ShouldShowCursor => false;
- protected Vector3 CameraPosition { get; set; }
- protected Rotation CameraRotation { get; set; }
- protected float FieldOfView { get; set; }
- protected IEntity FirstPersonViewer { get; set; }
- protected Transform SoundSource { get; set; }
+ public Vector3 CameraPosition { get; set; }
+ public Rotation CameraRotation { get; set; }
+ public float FieldOfView { get; set; }
+ public IEntity FirstPersonViewer { get; set; }
+ public Transform SoundSource { get; set; }
///
/// Handles any input-independent camera updates (ie following a pawn)
diff --git a/code/EntityComponents/Lucker/Cameras/FpsCamera.cs b/code/EntityComponents/Lucker/Cameras/FpsCamera.cs
new file mode 100644
index 0000000..d5d2284
--- /dev/null
+++ b/code/EntityComponents/Lucker/Cameras/FpsCamera.cs
@@ -0,0 +1,43 @@
+using Sandbox;
+
+namespace LuckerGame.Components.Lucker.Cameras;
+
+public class FpsCamera : AbstractCamera
+{
+ protected override void UpdateCameraParameters()
+ {
+ if ( Entity.Pawn is not Minigames.FpsTest.Pawn pawn)
+ {
+ return;
+ }
+
+ CameraRotation = pawn.ViewAngles.ToRotation();
+ FieldOfView = Screen.CreateVerticalFieldOfView( Game.Preferences.FieldOfView );
+ FirstPersonViewer = pawn;
+ CameraPosition = pawn.EyePosition;
+ }
+
+ public override void BuildInput()
+ {
+ if ( Input.StopProcessing )
+ return;
+
+ if ( Entity.Pawn is not Minigames.FpsTest.Pawn pawn )
+ {
+ return;
+ }
+
+ var look = Input.AnalogLook;
+
+ if ( pawn.ViewAngles.pitch is > 90f or < -90f )
+ {
+ look = look.WithYaw( look.yaw * -1f );
+ }
+
+ var viewAngles = pawn.ViewAngles;
+ viewAngles += look;
+ viewAngles.pitch = viewAngles.pitch.Clamp( -89f, 89f );
+ viewAngles.roll = 0f;
+ pawn.ViewAngles = viewAngles.Normal;
+ }
+}
diff --git a/code/EntityComponents/Lucker/Cameras/RTSCamera.cs b/code/EntityComponents/Lucker/Cameras/RTSCamera.cs
index e107fba..b9b4243 100644
--- a/code/EntityComponents/Lucker/Cameras/RTSCamera.cs
+++ b/code/EntityComponents/Lucker/Cameras/RTSCamera.cs
@@ -7,7 +7,7 @@ namespace LuckerGame.Components.Lucker.Cameras;
///
/// A top down camera that can be
///
-public partial class RTSCamera : AbstractCamera, ISingletonComponent
+public partial class RTSCamera : AbstractCamera
{
public override bool ShouldShowCursor => true;
private const float MaxDistance = 400f;
diff --git a/code/Minigames/FpsTest/FpsTestMinigame.cs b/code/Minigames/FpsTest/FpsTestMinigame.cs
new file mode 100644
index 0000000..a518a20
--- /dev/null
+++ b/code/Minigames/FpsTest/FpsTestMinigame.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using LuckerGame.Components.Lucker.Cameras;
+using LuckerGame.Entities;
+using Sandbox;
+
+namespace LuckerGame.Minigames.FpsTest;
+
+[Library("mg_fps_test")]
+public class FpsTestMinigame : Minigame
+{
+ public override string Name => "FPS Test";
+ private List Players { get; set; }
+
+ public override void Initialize( List players )
+ {
+ Players = players;
+ Players.ForEach( player =>
+ {
+ player.Components.Create();
+ Pawn fpsPawn = new Pawn();
+ fpsPawn.SetupOwner(player);
+ player.Pawn = fpsPawn;
+
+ // Get all of the spawnpoints
+ var spawnpoints = Entity.All.OfType();
+
+ // chose a random one
+ var randomSpawnPoint = spawnpoints.OrderBy( x => Guid.NewGuid() ).FirstOrDefault();
+
+ // if it exists, place the pawn there
+ if ( randomSpawnPoint != null )
+ {
+ var tx = randomSpawnPoint.Transform;
+ tx.Position = tx.Position + Vector3.Up * 50.0f; // raise it up
+ player.Position = tx.Position;
+ }
+ } );
+ }
+
+ public override void Tick()
+ {
+ }
+
+ public override void Cleanup()
+ {
+ }
+}
diff --git a/code/Minigames/FpsTest/Pawn.cs b/code/Minigames/FpsTest/Pawn.cs
new file mode 100644
index 0000000..9214303
--- /dev/null
+++ b/code/Minigames/FpsTest/Pawn.cs
@@ -0,0 +1,164 @@
+using Sandbox;
+using System.ComponentModel;
+using LuckerGame.Minigames.FpsTest.Weapons;
+
+namespace LuckerGame.Minigames.FpsTest;
+
+public partial class Pawn : AnimatedEntity
+{
+ [Net, Predicted]
+ public FpsWeapon ActiveFpsWeapon { get; set; }
+
+ public Vector3 InputDirection
+ {
+ get => PropertyProxy?.InputDirection ?? Vector3.Zero;
+ set
+ {
+ if ( PropertyProxy == null ) return;
+ PropertyProxy.InputDirection = value;
+ }
+ }
+
+ public Angles ViewAngles {
+ get => PropertyProxy?.ViewAngles ?? Angles.Zero;
+ set
+ {
+ if ( PropertyProxy == null ) return;
+ PropertyProxy.ViewAngles = value;
+ }
+ }
+
+ ///
+ /// Position a player should be looking from in world space.
+ ///
+ [Browsable( false )]
+ public Vector3 EyePosition
+ {
+ get => Transform.PointToWorld( EyeLocalPosition );
+ set => EyeLocalPosition = Transform.PointToLocal( value );
+ }
+
+ ///
+ /// Position a player should be looking from in local to the entity coordinates.
+ ///
+ [Net, Predicted, Browsable( false )]
+ public Vector3 EyeLocalPosition { get; set; }
+
+ ///
+ /// Rotation of the entity's "eyes", i.e. rotation for the camera when this entity is used as the view entity.
+ ///
+ [Browsable( false )]
+ public Rotation EyeRotation
+ {
+ get => Transform.RotationToWorld( EyeLocalRotation );
+ set => EyeLocalRotation = Transform.RotationToLocal( value );
+ }
+
+ ///
+ /// Rotation of the entity's "eyes", i.e. rotation for the camera when this entity is used as the view entity. In local to the entity coordinates.
+ ///
+ [Net, Predicted, Browsable( false )]
+ public Rotation EyeLocalRotation { get; set; }
+
+ public BBox Hull
+ {
+ get => new
+ (
+ new Vector3( -16, -16, 0 ),
+ new Vector3( 16, 16, 64 )
+ );
+ }
+
+ [BindComponent] public PawnController Controller { get; }
+ [BindComponent] public PawnAnimator Animator { get; }
+
+ public override Ray AimRay => new Ray( EyePosition, EyeRotation.Forward );
+
+ private PawnPropertyProxyComponent PropertyProxy { get => Owner?.Components.Get(); }
+
+ ///
+ /// Called when the entity is first created
+ ///
+ public override void Spawn()
+ {
+
+ SetModel( "models/citizen/citizen.vmdl" );
+
+ EnableDrawing = true;
+ EnableHideInFirstPerson = true;
+ EnableShadowInFirstPerson = true;
+
+ Components.Create();
+ Components.Create();
+ }
+
+ public void SetActiveWeapon( FpsWeapon fpsWeapon )
+ {
+ ActiveFpsWeapon?.OnHolster();
+ ActiveFpsWeapon = fpsWeapon;
+ ActiveFpsWeapon.OnEquip( this );
+ }
+
+ public void DressFromClient( IClient cl )
+ {
+ var c = new ClothingContainer();
+ c.LoadFromClient( cl );
+ c.DressEntity( this );
+ }
+
+ public override void Simulate( IClient cl )
+ {
+ SimulateRotation();
+ Controller?.Simulate( cl );
+ Animator?.Simulate();
+ ActiveFpsWeapon?.Simulate( cl );
+ EyeLocalPosition = Vector3.Up * (64f * Scale);
+ }
+
+ public override void BuildInput()
+ {
+ InputDirection = Input.AnalogMove;
+
+ if ( Input.StopProcessing )
+ return;
+ }
+
+ public override void FrameSimulate( IClient cl )
+ {
+ SimulateRotation();
+ }
+
+ public TraceResult TraceBBox( Vector3 start, Vector3 end, float liftFeet = 0.0f )
+ {
+ return TraceBBox( start, end, Hull.Mins, Hull.Maxs, liftFeet );
+ }
+
+ public TraceResult TraceBBox( Vector3 start, Vector3 end, Vector3 mins, Vector3 maxs, float liftFeet = 0.0f )
+ {
+ if ( liftFeet > 0 )
+ {
+ start += Vector3.Up * liftFeet;
+ maxs = maxs.WithZ( maxs.z - liftFeet );
+ }
+
+ var tr = Trace.Ray( start, end )
+ .Size( mins, maxs )
+ .WithAnyTags( "solid", "playerclip", "passbullets" )
+ .Ignore( this )
+ .Run();
+
+ return tr;
+ }
+
+ public void SetupOwner( Entity owner )
+ {
+ Owner = owner;
+ owner.Components.GetOrCreate();
+ }
+
+ protected void SimulateRotation()
+ {
+ EyeRotation = ViewAngles.ToRotation();
+ Rotation = ViewAngles.WithPitch( 0f ).ToRotation();
+ }
+}
diff --git a/code/Minigames/FpsTest/PawnAnimator.cs b/code/Minigames/FpsTest/PawnAnimator.cs
new file mode 100644
index 0000000..ad062cc
--- /dev/null
+++ b/code/Minigames/FpsTest/PawnAnimator.cs
@@ -0,0 +1,20 @@
+using Sandbox;
+
+namespace LuckerGame.Minigames.FpsTest;
+
+public class PawnAnimator : EntityComponent, ISingletonComponent
+{
+ public void Simulate()
+ {
+ var helper = new CitizenAnimationHelper( Entity );
+ helper.WithVelocity( Entity.Velocity );
+ helper.WithLookAt( Entity.EyePosition + Entity.EyeRotation.Forward * 100 );
+ helper.HoldType = CitizenAnimationHelper.HoldTypes.None;
+ helper.IsGrounded = Entity.GroundEntity.IsValid();
+
+ if ( Entity.Controller.HasEvent( "jump" ) )
+ {
+ helper.TriggerJump();
+ }
+ }
+}
diff --git a/code/Minigames/FpsTest/PawnController.cs b/code/Minigames/FpsTest/PawnController.cs
new file mode 100644
index 0000000..60e035c
--- /dev/null
+++ b/code/Minigames/FpsTest/PawnController.cs
@@ -0,0 +1,174 @@
+using Sandbox;
+using System;
+using System.Collections.Generic;
+
+namespace LuckerGame.Minigames.FpsTest;
+
+public class PawnController : EntityComponent
+{
+ public int StepSize => 24;
+ public int GroundAngle => 45;
+ public int JumpSpeed => 300;
+ public float Gravity => 800f;
+
+ HashSet ControllerEvents = new( StringComparer.OrdinalIgnoreCase );
+
+ bool Grounded => Entity.GroundEntity.IsValid();
+
+ public void Simulate( IClient cl )
+ {
+ ControllerEvents.Clear();
+
+ var movement = Entity.InputDirection.Normal;
+ var angles = Entity.ViewAngles.WithPitch( 0 );
+ var moveVector = Rotation.From( angles ) * movement * 320f;
+ var groundEntity = CheckForGround();
+
+ if ( groundEntity.IsValid() )
+ {
+ if ( !Grounded )
+ {
+ Entity.Velocity = Entity.Velocity.WithZ( 0 );
+ AddEvent( "grounded" );
+ }
+
+ Entity.Velocity = Accelerate( Entity.Velocity, moveVector.Normal, moveVector.Length, 200.0f * ( Input.Down( "run" ) ? 2.5f : 1f ), 7.5f );
+ Entity.Velocity = ApplyFriction( Entity.Velocity, 4.0f );
+ }
+ else
+ {
+ Entity.Velocity = Accelerate( Entity.Velocity, moveVector.Normal, moveVector.Length, 100, 20f );
+ Entity.Velocity += Vector3.Down * Gravity * Time.Delta;
+ }
+
+ if ( Input.Pressed( "jump" ) )
+ {
+ DoJump();
+ }
+
+ var mh = new MoveHelper( Entity.Position, Entity.Velocity );
+ mh.Trace = mh.Trace.Size( Entity.Hull ).Ignore( Entity );
+
+ if ( mh.TryMoveWithStep( Time.Delta, StepSize ) > 0 )
+ {
+ if ( Grounded )
+ {
+ mh.Position = StayOnGround( mh.Position );
+ }
+ Entity.Position = mh.Position;
+ Entity.Velocity = mh.Velocity;
+ }
+
+ Entity.GroundEntity = groundEntity;
+ }
+
+ void DoJump()
+ {
+ if ( Grounded )
+ {
+ Entity.Velocity = ApplyJump( Entity.Velocity, "jump" );
+ }
+ }
+
+ Entity CheckForGround()
+ {
+ if ( Entity.Velocity.z > 100f )
+ return null;
+
+ var trace = Entity.TraceBBox( Entity.Position, Entity.Position + Vector3.Down, 2f );
+
+ if ( !trace.Hit )
+ return null;
+
+ if ( trace.Normal.Angle( Vector3.Up ) > GroundAngle )
+ return null;
+
+ return trace.Entity;
+ }
+
+ Vector3 ApplyFriction( Vector3 input, float frictionAmount )
+ {
+ float StopSpeed = 100.0f;
+
+ var speed = input.Length;
+ if ( speed < 0.1f ) return input;
+
+ // Bleed off some speed, but if we have less than the bleed
+ // threshold, bleed the threshold amount.
+ float control = (speed < StopSpeed) ? StopSpeed : speed;
+
+ // Add the amount to the drop amount.
+ var drop = control * Time.Delta * frictionAmount;
+
+ // scale the velocity
+ float newspeed = speed - drop;
+ if ( newspeed < 0 ) newspeed = 0;
+ if ( newspeed == speed ) return input;
+
+ newspeed /= speed;
+ input *= newspeed;
+
+ return input;
+ }
+
+ Vector3 Accelerate( Vector3 input, Vector3 wishdir, float wishspeed, float speedLimit, float acceleration )
+ {
+ if ( speedLimit > 0 && wishspeed > speedLimit )
+ wishspeed = speedLimit;
+
+ var currentspeed = input.Dot( wishdir );
+ var addspeed = wishspeed - currentspeed;
+
+ if ( addspeed <= 0 )
+ return input;
+
+ var accelspeed = acceleration * Time.Delta * wishspeed;
+
+ if ( accelspeed > addspeed )
+ accelspeed = addspeed;
+
+ input += wishdir * accelspeed;
+
+ return input;
+ }
+
+ Vector3 ApplyJump( Vector3 input, string jumpType )
+ {
+ AddEvent( jumpType );
+
+ return input + Vector3.Up * JumpSpeed;
+ }
+
+ Vector3 StayOnGround( Vector3 position )
+ {
+ var start = position + Vector3.Up * 2;
+ var end = position + Vector3.Down * StepSize;
+
+ // See how far up we can go without getting stuck
+ var trace = Entity.TraceBBox( position, start );
+ start = trace.EndPosition;
+
+ // Now trace down from a known safe position
+ trace = Entity.TraceBBox( start, end );
+
+ if ( trace.Fraction <= 0 ) return position;
+ if ( trace.Fraction >= 1 ) return position;
+ if ( trace.StartedSolid ) return position;
+ if ( Vector3.GetAngle( Vector3.Up, trace.Normal ) > GroundAngle ) return position;
+
+ return trace.EndPosition;
+ }
+
+ public bool HasEvent( string eventName )
+ {
+ return ControllerEvents.Contains( eventName );
+ }
+
+ void AddEvent( string eventName )
+ {
+ if ( HasEvent( eventName ) )
+ return;
+
+ ControllerEvents.Add( eventName );
+ }
+}
diff --git a/code/Minigames/FpsTest/PawnPropertyProxyComponent.cs b/code/Minigames/FpsTest/PawnPropertyProxyComponent.cs
new file mode 100644
index 0000000..2702831
--- /dev/null
+++ b/code/Minigames/FpsTest/PawnPropertyProxyComponent.cs
@@ -0,0 +1,22 @@
+using System.ComponentModel;
+using LuckerGame.Entities;
+using Sandbox;
+
+namespace LuckerGame.Minigames.FpsTest;
+
+///
+/// Component attached to a Lucker that will allow us to use [ClientInput] in the actual minigame's Pawn.
+///
+/// <
+///
+/// This is because [ClientInput] only works for the Pawn owned by the Client (Lucker), and the Lucker's components.
+/// We create this component in the Minigame's Pawn, attach it to the Lucker, and use these properties from there.
+///
+public class PawnPropertyProxyComponent : EntityComponent
+{
+ [ClientInput]
+ public Vector3 InputDirection { get; set; }
+
+ [ClientInput]
+ public Angles ViewAngles { get; set; }
+}
diff --git a/code/Minigames/FpsTest/Weapons/FpsWeapon.cs b/code/Minigames/FpsTest/Weapons/FpsWeapon.cs
new file mode 100644
index 0000000..251ce19
--- /dev/null
+++ b/code/Minigames/FpsTest/Weapons/FpsWeapon.cs
@@ -0,0 +1,205 @@
+using System.Collections.Generic;
+using Sandbox;
+
+namespace LuckerGame.Minigames.FpsTest.Weapons;
+
+public partial class FpsWeapon : AnimatedEntity
+{
+ ///
+ /// The View Model's entity, only accessible clientside.
+ ///
+ public WeaponViewModel ViewModelEntity { get; protected set; }
+
+ ///
+ /// An accessor to grab our Pawn.
+ ///
+ public Pawn Pawn => Owner as Pawn;
+
+ ///
+ /// This'll decide which entity to fire effects from. If we're in first person, the View Model, otherwise, this.
+ ///
+ public AnimatedEntity EffectEntity => Camera.FirstPersonViewer == Owner ? ViewModelEntity : this;
+
+ public virtual string ViewModelPath => null;
+ public virtual string ModelPath => null;
+
+ ///
+ /// How often you can shoot this gun.
+ ///
+ public virtual float PrimaryRate => 5.0f;
+
+ ///
+ /// How long since we last shot this gun.
+ ///
+ [Net, Predicted] public TimeSince TimeSincePrimaryAttack { get; set; }
+
+ public override void Spawn()
+ {
+ EnableHideInFirstPerson = true;
+ EnableShadowInFirstPerson = true;
+ EnableDrawing = false;
+
+ if ( ModelPath != null )
+ {
+ SetModel( ModelPath );
+ }
+ }
+
+ ///
+ /// Called when is called for this weapon.
+ ///
+ ///
+ public void OnEquip( Pawn pawn )
+ {
+ Owner = pawn;
+ SetParent( pawn, true );
+ EnableDrawing = true;
+ CreateViewModel( To.Single( pawn ) );
+ }
+
+ ///
+ /// Called when the weapon is either removed from the player, or holstered.
+ ///
+ public void OnHolster()
+ {
+ EnableDrawing = false;
+ DestroyViewModel( To.Single( Owner ) );
+ }
+
+ ///
+ /// Called from .
+ ///
+ ///
+ public override void Simulate( IClient player )
+ {
+ Animate();
+
+ if ( CanPrimaryAttack() )
+ {
+ using ( LagCompensation() )
+ {
+ TimeSincePrimaryAttack = 0;
+ PrimaryAttack();
+ }
+ }
+ }
+
+ ///
+ /// Called every to see if we can shoot our gun.
+ ///
+ ///
+ public virtual bool CanPrimaryAttack()
+ {
+ if ( !Owner.IsValid() || !Input.Down( "attack1" ) ) return false;
+
+ var rate = PrimaryRate;
+ if ( rate <= 0 ) return true;
+
+ return TimeSincePrimaryAttack > (1 / rate);
+ }
+
+ ///
+ /// Called when your gun shoots.
+ ///
+ public virtual void PrimaryAttack()
+ {
+ }
+
+ ///
+ /// Useful for setting anim parameters based off the current weapon.
+ ///
+ protected virtual void Animate()
+ {
+ }
+
+ ///
+ /// Does a trace from start to end, does bullet impact effects. Coded as an IEnumerable so you can return multiple
+ /// hits, like if you're going through layers or ricocheting or something.
+ ///
+ public virtual IEnumerable TraceBullet( Vector3 start, Vector3 end, float radius = 2.0f )
+ {
+ bool underWater = Trace.TestPoint( start, "water" );
+
+ var trace = Trace.Ray( start, end )
+ .UseHitboxes()
+ .WithAnyTags( "solid", "player", "npc" )
+ .Ignore( this )
+ .Size( radius );
+
+ //
+ // If we're not underwater then we can hit water
+ //
+ if ( !underWater )
+ trace = trace.WithAnyTags( "water" );
+
+ var tr = trace.Run();
+
+ if ( tr.Hit )
+ yield return tr;
+ }
+
+ ///
+ /// Shoot a single bullet
+ ///
+ public virtual void ShootBullet( Vector3 pos, Vector3 dir, float spread, float force, float damage, float bulletSize )
+ {
+ var forward = dir;
+ forward += (Vector3.Random + Vector3.Random + Vector3.Random + Vector3.Random) * spread * 0.25f;
+ forward = forward.Normal;
+
+ //
+ // ShootBullet is coded in a way where we can have bullets pass through shit
+ // or bounce off shit, in which case it'll return multiple results
+ //
+ foreach ( var tr in TraceBullet( pos, pos + forward * 5000, bulletSize ) )
+ {
+ tr.Surface.DoBulletImpact( tr );
+
+ if ( !Game.IsServer ) continue;
+ if ( !tr.Entity.IsValid() ) continue;
+
+ //
+ // We turn predictiuon off for this, so any exploding effects don't get culled etc
+ //
+ using ( Prediction.Off() )
+ {
+ var damageInfo = DamageInfo.FromBullet( tr.EndPosition, forward * 100 * force, damage )
+ .UsingTraceResult( tr )
+ .WithAttacker( Owner )
+ .WithWeapon( this );
+
+ tr.Entity.TakeDamage( damageInfo );
+ }
+ }
+ }
+
+ ///
+ /// Shoot a single bullet from owners view point
+ ///
+ public virtual void ShootBullet( float spread, float force, float damage, float bulletSize )
+ {
+ Game.SetRandomSeed( Time.Tick );
+
+ var ray = Owner.AimRay;
+ ShootBullet( ray.Position, ray.Forward, spread, force, damage, bulletSize );
+ }
+
+ [ClientRpc]
+ private void CreateViewModel()
+ {
+ if ( ViewModelPath == null ) return;
+
+ var vm = new WeaponViewModel( this );
+ vm.Model = Model.Load( ViewModelPath );
+ ViewModelEntity = vm;
+ }
+
+ [ClientRpc]
+ public void DestroyViewModel()
+ {
+ if ( ViewModelEntity.IsValid() )
+ {
+ ViewModelEntity.Delete();
+ }
+ }
+}
diff --git a/code/Minigames/FpsTest/Weapons/Pistol.cs b/code/Minigames/FpsTest/Weapons/Pistol.cs
new file mode 100644
index 0000000..f68ef54
--- /dev/null
+++ b/code/Minigames/FpsTest/Weapons/Pistol.cs
@@ -0,0 +1,32 @@
+using Sandbox;
+
+namespace LuckerGame.Minigames.FpsTest.Weapons;
+
+public partial class Pistol : FpsWeapon
+{
+ public override string ModelPath => "weapons/rust_pistol/rust_pistol.vmdl";
+ public override string ViewModelPath => "weapons/rust_pistol/v_rust_pistol.vmdl";
+
+ [ClientRpc]
+ protected virtual void ShootEffects()
+ {
+ Game.AssertClient();
+
+ Particles.Create( "particles/pistol_muzzleflash.vpcf", EffectEntity, "muzzle" );
+
+ Pawn.SetAnimParameter( "b_attack", true );
+ ViewModelEntity?.SetAnimParameter( "fire", true );
+ }
+
+ public override void PrimaryAttack()
+ {
+ ShootEffects();
+ Pawn.PlaySound( "rust_pistol.shoot" );
+ ShootBullet( 0.1f, 100, 20, 1 );
+ }
+
+ protected override void Animate()
+ {
+ Pawn.SetAnimParameter( "holdtype", (int)CitizenAnimationHelper.HoldTypes.Pistol );
+ }
+}
diff --git a/code/Minigames/FpsTest/Weapons/WeaponViewModel.cs b/code/Minigames/FpsTest/Weapons/WeaponViewModel.cs
new file mode 100644
index 0000000..1bfc004
--- /dev/null
+++ b/code/Minigames/FpsTest/Weapons/WeaponViewModel.cs
@@ -0,0 +1,22 @@
+using Sandbox;
+
+namespace LuckerGame.Minigames.FpsTest.Weapons;
+
+public partial class WeaponViewModel : BaseViewModel
+{
+ protected FpsWeapon FpsWeapon { get; init; }
+
+ public WeaponViewModel( FpsWeapon fpsWeapon )
+ {
+ FpsWeapon = fpsWeapon;
+ EnableShadowCasting = false;
+ EnableViewmodelRendering = true;
+ }
+
+ public override void PlaceViewmodel()
+ {
+ base.PlaceViewmodel();
+
+ Camera.Main.SetViewModelCamera( 80f, 1, 500 );
+ }
+}
diff --git a/code/UI/HudComponents/CameraCursor.razor b/code/UI/HudComponents/CameraCursor.razor
index e32e736..a24cc16 100644
--- a/code/UI/HudComponents/CameraCursor.razor
+++ b/code/UI/HudComponents/CameraCursor.razor
@@ -1,3 +1,4 @@
+@using System
@using LuckerGame.Components.Lucker.Cameras
@using Sandbox
@using Sandbox.UI
@@ -5,12 +6,19 @@
@attribute [StyleSheet]
@inherits Panel
-@if (ShouldShowCursor)
-{
-
-}
+
+ @if (ShouldShowCursor)
+ {
+
+ }
+
@code {
+ protected override int BuildHash()
+ {
+ return HashCode.Combine(ShouldShowCursor);
+ }
+
private bool ShouldShowCursor => Game.LocalClient.Pawn?.Components.Get()?.ShouldShowCursor ?? false;
}
\ No newline at end of file
diff --git a/code/UI/HudComponents/CameraCursor.razor.scss b/code/UI/HudComponents/CameraCursor.razor.scss
index 82e3b95..7316c9f 100644
--- a/code/UI/HudComponents/CameraCursor.razor.scss
+++ b/code/UI/HudComponents/CameraCursor.razor.scss
@@ -1,3 +1,5 @@
CameraCursor {
- pointer-events: all;
+ .cursor-catch {
+ pointer-events: all;
+ }
}
\ No newline at end of file