@ -1,83 +1,65 @@
using System ;
using Godot ;
// TODO: "Click" sound when attempting to fire when not ready, or empty.
// TODO: "Reload" sound when reloading.
// TODO: "Single reload" for revolver & shotgun.
// TODO: Add outline around sprites.
public class Weapon : Sprite
{
[Export] public int EffectiveRange { get ; set ; } = 3 2 0 ;
[Export] public int MaximumRange { get ; set ; } = 6 4 0 ;
[Export] public float Knockback { get ; set ; } = 0.0F ;
[Export] public bool Automatic { get ; set ; } = false ;
[Export] public int RateOfFire { get ; set ; } = 1 0 0 ; // rounds/minute
[Export] public int Capacity { get ; set ; } = 1 2 ;
[Export] public float ReloadTime { get ; set ; } = 1.0F ;
[Export] public float Knockback { get ; set ; } = 0.0F ;
[Export] public float Spread { get ; set ; } = 0.0F ;
[Export] public float SpreadIncrease { get ; set ; } = 0.0F ;
[Export] public float RecoilMin { get ; set ; } = 0.0F ;
[Export] public float RecoilMax { get ; set ; } = 0.0F ;
[Export] public int BulletSpeed { get ; set ; } = 2 0 0 0 ;
[Export] public int BulletsPetShot { get ; set ; } = 1 ;
[Export] public int EffectiveRange { get ; set ; } = 3 2 0 ;
[Export] public int MaximumRange { get ; set ; } = 6 4 0 ;
[Export] public int BulletVelocity { get ; set ; } = 2 0 0 0 ;
[Export] public int BulletsPerShot { get ; set ; } = 1 ;
[Export] public float BulletOpacity { get ; set ; } = 0.2F ;
public Cursor Cursor { get ; private set ; }
public Player Player { get ; private set ; }
public float _f ireDelay ;
public float? _ reloading ;
private float _ currentSpreadInc = 0.0F ;
private float _ currentRecoil = 0.0F ;
public int Rounds { get ; private set ; }
public float AimDirection { get ; private set ; }
public TimeSpan ? HoldingTrigger { get ; private set ; }
// TODO: Tell the server when we're pressing/releasing the trigger.
public float? ReloadProgress = > 1 - _ reloading / ReloadTime ;
public Cursor Cursor { get ; private set ; }
public Player Player { get ; private set ; }
public override void _ Ready ( )
{
Rounds = Capacity ;
Cursor = this . GetClient ( ) ? . Cursor ;
Player = GetParent ( ) . GetParent < Player > ( ) ;
}
public override void _ UnhandledInput ( InputEvent ev )
{
if ( ! ( Player is LocalPlayer localPlayer ) ) return ;
if ( ev . IsActionPressed ( "interact_primary" ) ) {
var seed = unchecked ( ( int ) GD . Randi ( ) ) ;
ShootInternal ( AimDirection , Scale . y > 0 , seed ) ;
RpcId ( 1 , nameof ( Shoot ) , AimDirection , Scale . y > 0 , seed ) ;
localPlayer . Velocity - = Mathf . Polar2Cartesian ( Knockback , Rotation ) ;
}
}
[Remote]
private void Shoot ( float aimDirection , bool toRight , int seed )
{
if ( this . GetGame ( ) is Server ) {
if ( Player . NetworkID ! = GetTree ( ) . GetRpcSenderId ( ) ) return ;
// TODO: Verify input.
Rpc ( nameof ( Shoot ) , aimDirection , toRight , seed ) ;
} else if ( Player is LocalPlayer ) return ;
ShootInternal ( aimDirection , toRight , seed ) ;
}
private void ShootInternal ( float aimDirection , bool toRight , int seed )
{
if ( this . GetGame ( ) is Client )
GetNodeOrNull < AudioStreamPlayer2D > ( "Fire" ) ? . Play ( ) ;
var random = new Random ( seed ) ;
var angle = aimDirection - _ currentRecoil * ( toRight ? 1 : - 1 ) ;
var tip = GetNode < Node2D > ( "Tip" ) . Position ;
if ( ! toRight ) tip . y * = - 1 ;
tip = tip . Rotated ( angle ) ;
for ( var i = 0 ; i < BulletsPetShot ; i + + ) {
var spread = ( Mathf . Deg2Rad ( Spread ) + _ currentSpreadInc ) * Mathf . Clamp ( random . NextGaussian ( 0.4F ) , - 1 , 1 ) ;
var dir = Mathf . Polar2Cartesian ( 1 , angle + spread ) ;
var color = new Color ( Player . Color , BulletOpacity ) ;
var bullet = new Bullet ( Player . Position + tip , dir , EffectiveRange , MaximumRange , BulletSpeed , color ) ;
this . GetWorld ( ) . AddChild ( bullet ) ;
if ( ! ( Player is LocalPlayer ) ) return ;
if ( ev . IsActionPressed ( "interact_primary" ) )
{ HoldingTrigger = TimeSpan . Zero ; OnTriggerPressed ( ) ; }
if ( ev . IsActionPressed ( "interact_reload" ) & & ( Rounds < Capacity ) & & ( _ reloading = = null ) )
{ _ reloading = ReloadTime ; }
}
_ currentSpreadInc + = Mathf . Deg2Rad ( SpreadIncrease ) ;
_ currentRecoil + = Mathf . Deg2Rad ( random . NextFloat ( RecoilMin , RecoilMax ) ) ;
}
protected virtual void OnTriggerPressed ( ) = > Fire ( ) ;
protected virtual void OnTriggerReleased ( ) { }
public override void _ Process ( float delta )
{
@ -86,11 +68,45 @@ public class Weapon : Sprite
_ currentSpreadInc = Mathf . Max ( 0 , _ currentSpreadInc - spreadDecrease * delta ) ;
_ currentRecoil = Mathf . Max ( 0 , _ currentRecoil - recoilDecrease * delta ) ;
if ( Visible & & ( Player is LocalPlayer ) ) {
if ( Visible ) {
if ( HoldingTrigger is TimeSpan holding )
HoldingTrigger = holding + TimeSpan . FromSeconds ( delta ) ;
if ( _ reloading is float reloading ) {
_ reloading = reloading - delta ;
if ( _ reloading < = 0 ) {
Rounds = Capacity ;
_ reloading = null ;
}
} else if ( Rounds < = 0 )
// Automatically reload when out of rounds.
_ reloading = ReloadTime ;
if ( _f ireDelay > 0 ) {
_f ireDelay - = delta ;
// We allow _fireDelay to go into negatives to allow
// for more accurate rate of fire for automatic weapons.
// Though, if the trigger isn't held down, reset it to 0.
if ( ( _f ireDelay < 0 ) & & ( ! Automatic | | ( HoldingTrigger = = null ) ) )
_f ireDelay = 0 ;
}
if ( Player is LocalPlayer ) {
if ( HoldingTrigger ! = null ) {
if ( ! Input . IsActionPressed ( "interact_primary" ) ) {
HoldingTrigger = null ;
OnTriggerReleased ( ) ;
} else if ( Automatic )
Fire ( ) ;
}
AimDirection = Cursor . Position . AngleToPoint ( Player . Position ) ;
RpcId ( 1 , nameof ( SendAimAngle ) , AimDirection ) ;
Update ( ) ;
}
} else {
_ reloading = null ;
}
var angle = Mathf . PosMod ( AimDirection + Mathf . Pi , Mathf . Tau ) - Mathf . Pi ;
angle = Mathf . Abs ( Mathf . Rad2Deg ( angle ) ) ;
@ -99,21 +115,10 @@ public class Weapon : Sprite
Rotation = AimDirection - _ currentRecoil * ( ( Scale . y > 0 ) ? 1 : - 1 ) ;
}
[Remote]
private void SendAimAngle ( float value )
{
if ( this . GetGame ( ) is Server ) {
if ( Player . NetworkID ! = GetTree ( ) . GetRpcSenderId ( ) ) return ;
// TODO: Verify input.
// if ((value < 0) || (value > Mathf.Tau)) return;
Rpc ( nameof ( SendAimAngle ) , value ) ;
} else if ( ! ( Player is LocalPlayer ) )
AimDirection = value ;
}
public override void _D raw ( )
{
if ( ! ( Player is LocalPlayer ) ) return ;
// Draws an "aiming cone" to show where bullets might travel.
var tip = GetNode < Node2D > ( "Tip" ) . Position + new Vector2 ( 4 , 0 ) ;
var angle = Mathf . Sin ( ( Mathf . Deg2Rad ( Spread ) + _ currentSpreadInc ) / 2 ) ;
@ -153,4 +158,70 @@ public class Weapon : Sprite
}
private static Vector3 To3 ( Vector2 vec )
= > new Vector3 ( vec . x , vec . y , 0 ) ;
[Remote]
private void SendAimAngle ( float value )
{
if ( this . GetGame ( ) is Server ) {
if ( Player . NetworkID ! = GetTree ( ) . GetRpcSenderId ( ) ) return ;
// TODO: Verify input.
// if ((value < 0) || (value > Mathf.Tau)) return;
Rpc ( nameof ( SendAimAngle ) , value ) ;
} else if ( ! ( Player is LocalPlayer ) )
AimDirection = value ;
}
private void Fire ( )
{
var seed = unchecked ( ( int ) GD . Randi ( ) ) ;
if ( ! FireInternal ( AimDirection , Scale . y > 0 , seed ) ) return ;
RpcId ( 1 , nameof ( Fire ) , AimDirection , Scale . y > 0 , seed ) ;
( ( LocalPlayer ) Player ) . Velocity - = Mathf . Polar2Cartesian ( Knockback , Rotation ) ;
}
[Remote]
private void Fire ( float aimDirection , bool toRight , int seed )
{
if ( this . GetGame ( ) is Server ) {
if ( Player . NetworkID ! = GetTree ( ) . GetRpcSenderId ( ) ) return ;
// TODO: Verify input.
if ( FireInternal ( aimDirection , toRight , seed ) )
Rpc ( nameof ( Fire ) , aimDirection , toRight , seed ) ;
} else if ( ! ( Player is LocalPlayer ) )
FireInternal ( aimDirection , toRight , seed ) ;
}
protected virtual bool FireInternal ( float aimDirection , bool toRight , int seed )
{
if ( ( _ reloading ! = null ) | | ( Rounds < = 0 ) | | ( _f ireDelay > 0 ) ) return false ;
if ( this . GetGame ( ) is Client )
GetNodeOrNull < AudioStreamPlayer2D > ( "Fire" ) ? . Play ( ) ;
var random = new Random ( seed ) ;
var angle = aimDirection - _ currentRecoil * ( toRight ? 1 : - 1 ) ;
var tip = GetNode < Node2D > ( "Tip" ) . Position ;
if ( ! toRight ) tip . y * = - 1 ;
tip = tip . Rotated ( angle ) ;
for ( var i = 0 ; i < BulletsPerShot ; i + + ) {
var spread = ( Mathf . Deg2Rad ( Spread ) + _ currentSpreadInc ) * Mathf . Clamp ( random . NextGaussian ( 0.4F ) , - 1 , 1 ) ;
var dir = Mathf . Polar2Cartesian ( 1 , angle + spread ) ;
var color = new Color ( Player . Color , BulletOpacity ) ;
var bullet = new Bullet ( Player . Position + tip , dir , EffectiveRange , MaximumRange , BulletVelocity , color ) ;
this . GetWorld ( ) . AddChild ( bullet ) ;
}
_ currentSpreadInc + = Mathf . Deg2Rad ( SpreadIncrease ) ;
_ currentRecoil + = Mathf . Deg2Rad ( random . NextFloat ( RecoilMin , RecoilMax ) ) ;
if ( ( this . GetGame ( ) is Server ) | | ( Player is LocalPlayer ) ) {
// Do not keep track of fire rate or ammo for other players.
_f ireDelay + = 6 0.0F / RateOfFire ;
Rounds - = 1 ;
}
return true ;
}
}