Weapons/CrossbowWeapon.cs
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 );
}
[Rpc.Broadcast]
private void ShootEffects()
{
if ( Application.IsDedicatedServer ) return;
var ev = new IWeaponEvent.AttackEvent( ViewModel.IsValid() );
IWeaponEvent.PostToGameObject( GameObject.Root, x => x.OnAttack( ev ) );
GameObject.PlaySound( FireSound );
}
TimeSince TimeSinceShoot;
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();
ShootEffects();
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 );
}
}