CrossbowWeapon component for a game weapon. Handles firing (hitscan when zoomed, projectile when unzoomed), ammo use, zoom toggle with FOV and sensitivity adjustments, camera effects, HUD crosshair drawing, and spawning projectile and stuck bolt prefabs with networked RPCs and sound.
using Sandbox.Rendering;
using Sandbox.Utility;
public class CrossbowWeapon : BaseWeapon
{
const float ZoomedFOV = 20.0f;
[Property] public float TimeBetweenShots { get; set; } = 2f;
[Property] public SoundEvent FireSound { get; set; }
[Property] public GameObject ProjectilePrefab { get; set; }
[Property] public GameObject StuckPrefab { get; set; }
public bool IsZoomed { get; private set; }
float fieldOfView;
CommandList _commandList;
protected override void OnEnabled()
{
base.OnEnabled();
fieldOfView = Scene.Camera.FieldOfView;
_commandList = new CommandList( "Crossbow" ) { Flags = CommandList.Flag.Hud };
Scene.Camera.AddCommandList( _commandList, Stage.AfterPostProcess, 2000 );
}
protected override void OnDisabled()
{
base.OnDisabled();
Scene.Camera.FieldOfView = Preferences.FieldOfView;
Scene.Camera.RemoveCommandList( _commandList, Stage.AfterPostProcess );
IsZoomed = false;
}
public override void OnControl( Player player )
{
base.OnControl( player );
if ( Input.Down( "attack1" ) )
{
Shoot( player );
}
if ( Input.Pressed( "attack2" ) )
{
IsZoomed = !IsZoomed;
}
_commandList.Reset();
if ( IsZoomed )
{
DrawCrossbowScope();
}
}
[Rpc.Broadcast]
private void ShootEffects( Vector3 hitpoint, bool hit, Vector3 normal, GameObject hitObject )
{
if ( !Owner.IsValid() ) return;
if ( Application.IsDedicatedServer ) return;
var ev = new IWeaponEvent.AttackEvent( ViewModel.IsValid() );
IWeaponEvent.PostToGameObject( GameObject.Root, x => x.OnAttack( ev ) );
if ( hit )
{
var stuck = StuckPrefab.Clone( hitpoint, Rotation.LookAt( hitpoint - Owner.EyeTransform.Position ), Vector3.One );
stuck.SetParent( hitObject );
}
GameObject.PlaySound( FireSound );
}
/// <summary>
/// Effects for launching a bolt as a projectile - no hit to report, so no impact.
/// </summary>
[Rpc.Broadcast]
private void ProjectileShootEffects()
{
if ( Application.IsDedicatedServer ) return;
var ev = new IWeaponEvent.AttackEvent( ViewModel.IsValid() );
IWeaponEvent.PostToGameObject( GameObject.Root, x => x.OnAttack( ev ) );
GameObject.PlaySound( FireSound );
}
public void Shoot( Player player )
{
if ( !CanShoot() || !TakeAmmo( 1 ) )
{
TryAutoReload();
return;
}
TimeSinceShoot = 0;
AddShootDelay( TimeBetweenShots );
var forward = player.EyeTransform.Rotation.Forward;
forward = forward.Normal;
if ( IsZoomed )
{
var bulletRadius = GameSettings.CrossbowBulletRadius * GameSettings.BulletRadius;
var tr = Scene.Trace.Ray( player.EyeTransform.ForwardRay with { Forward = forward }, 4096 )
.IgnoreGameObjectHierarchy( player.GameObject )
.WithCollisionRules( "bullet" )
.Radius( bulletRadius )
.UseHitboxes()
.Run();
ShootEffects( tr.EndPosition, tr.Hit, tr.Normal, tr.GameObject );
TraceAttack( TraceAttackInfo.From( tr, 120 ) );
}
else
{
var tr = Scene.Trace.Ray( player.EyeTransform.ForwardRay with { Forward = forward }, 4096 )
.IgnoreGameObjectHierarchy( player.GameObject )
.WithCollisionRules( "bullet" )
.UseHitboxes()
.Run();
ProjectileShootEffects();
CreateProjectile( tr.StartPosition + tr.Direction * 64f, tr.Direction, 4096 );
}
player.Controller.EyeAngles += new Angles( Random.Shared.Float( -0.2f, -0.3f ), Random.Shared.Float( -0.1f, 0.1f ), 0 );
if ( !player.Controller.ThirdPerson && player.IsLocalPlayer )
{
var punchPitch = Random.Shared.Float( 45, 35 );
if ( IsZoomed ) punchPitch *= 0.2f;
new Sandbox.CameraNoise.Punch( new Vector3( punchPitch, Random.Shared.Float( -10, -5 ), 0 ), 1.5f, 2, 0.5f );
new Sandbox.CameraNoise.Shake( 1f, 0.6f );
}
}
/// <summary>
/// We want to control the camera fov
/// </summary>
public override void OnCameraSetup( Player player, Sandbox.CameraComponent camera )
{
if ( !player.Network.IsOwner || !Network.IsOwner ) return;
var delta = 1f - MathF.Pow( 0.5f, Time.Delta * 50f );
if ( IsZoomed )
{
fieldOfView = fieldOfView.LerpTo( ZoomedFOV, delta );
}
else
{
fieldOfView = fieldOfView.LerpTo( camera.FieldOfView, delta );
}
camera.FieldOfView = fieldOfView;
}
/// <summary>
/// We want to control the camera sens
/// </summary>
public override void OnCameraMove( Player player, ref Angles angles )
{
if ( IsZoomed )
{
angles *= fieldOfView / Preferences.FieldOfView;
}
}
/// <summary>
/// Creates the projectile with the host's permission
/// </summary>
/// <param name="start"></param>
/// <param name="direction"></param>
/// <param name="speed"></param>
[Rpc.Host]
void CreateProjectile( Vector3 start, Vector3 direction, float speed )
{
if ( !Owner.IsValid() ) return;
var go = ProjectilePrefab?.Clone( start );
var projectile = go.GetComponent<CrossbowProjectile>();
Assert.True( projectile.IsValid(), "ExplosiveProjectile not on projectile prefab" );
projectile.Instigator = Owner.PlayerData;
projectile.UpdateDirection( direction, speed );
go.NetworkSpawn();
}
public override void DrawCrosshair( HudPainter hud, Vector2 center )
{
var tss = TimeSinceShoot.Relative.Remap( 0, 0.2f, 1, 0 );
var gap = 32 + Easing.EaseOut( tss ) * 32;
var w = 2;
Color color = !CanShoot() ? UI.CrosshairInactive : UI.CrosshairActive;
float lineLength = 12;
float circleRadius = 4;
hud.DrawLine(
center + new Vector2( gap, 0 ),
center + new Vector2( gap + lineLength, lineLength ),
w, color );
hud.DrawLine(
center + new Vector2( gap, 0 ),
center + new Vector2( gap + lineLength, -lineLength ),
w, color );
hud.DrawLine(
center + new Vector2( -gap, 0 ),
center + new Vector2( -gap - lineLength, lineLength ),
w, color );
hud.DrawLine(
center + new Vector2( -gap, 0 ),
center + new Vector2( -gap - lineLength, -lineLength ),
w, color );
hud.DrawCircle( center, circleRadius, color );
}
private void DrawCrossbowScope()
{
var mat = Material.FromShader( "shaders/weapons/ui_crossbow_zoom.shader" );
var radius = MathX.Remap( fieldOfView, Preferences.FieldOfView, ZoomedFOV, 0f, 0.3f );
if ( TimeSinceShoot < 0.5f )
{
radius += (0.5f - TimeSinceShoot) / 0.5f * 0.015f;
}
_commandList.Attributes.Set( "AspectRatio", Screen.Aspect );
_commandList.Attributes.Set( "Radius", radius );
_commandList.Blit( mat );
}
}