You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
227 lines
8.8 KiB
227 lines
8.8 KiB
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 bool Automatic { get; set; } = false; |
|
[Export] public int RateOfFire { get; set; } = 100; // rounds/minute |
|
[Export] public int Capacity { get; set; } = 12; |
|
[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 EffectiveRange { get; set; } = 320; |
|
[Export] public int MaximumRange { get; set; } = 640; |
|
[Export] public int BulletVelocity { get; set; } = 2000; |
|
[Export] public int BulletsPerShot { get; set; } = 1; |
|
[Export] public float BulletOpacity { get; set; } = 0.2F; |
|
|
|
|
|
public float _fireDelay; |
|
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)) return; |
|
if (ev.IsActionPressed("interact_primary")) |
|
{ HoldingTrigger = TimeSpan.Zero; OnTriggerPressed(); } |
|
if (ev.IsActionPressed("interact_reload") && (Rounds < Capacity) && (_reloading == null)) |
|
{ _reloading = ReloadTime; } |
|
} |
|
|
|
protected virtual void OnTriggerPressed() => Fire(); |
|
protected virtual void OnTriggerReleased() { } |
|
|
|
public override void _Process(float delta) |
|
{ |
|
var spreadDecrease = Mathf.Max(Mathf.Tau / 300, _currentSpreadInc * 2); |
|
var recoilDecrease = Mathf.Max(Mathf.Tau / 800, _currentRecoil * 2); |
|
_currentSpreadInc = Mathf.Max(0, _currentSpreadInc - spreadDecrease * delta); |
|
_currentRecoil = Mathf.Max(0, _currentRecoil - recoilDecrease * delta); |
|
|
|
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 (_fireDelay > 0) { |
|
_fireDelay -= 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 ((_fireDelay < 0) && (!Automatic || (HoldingTrigger == null))) |
|
_fireDelay = 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)); |
|
if (Scale.y > 0) { if (angle > 100.0F) Scale = new Vector2(1, -1); } |
|
else if (angle < 80.0F) Scale = new Vector2(1, 1); |
|
Rotation = AimDirection - _currentRecoil * ((Scale.y > 0) ? 1 : -1); |
|
} |
|
|
|
public override void _Draw() |
|
{ |
|
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); |
|
var color = Colors.Black; |
|
|
|
var points = new Vector2[8]; |
|
var colors = new Color[8]; |
|
colors[0] = colors[7] = new Color(color, 0.0F); |
|
points[0] = points[7] = tip; |
|
colors[1] = colors[6] = new Color(color, 0.15F); |
|
points[1] = tip + new Vector2(1, angle) * 64; |
|
points[6] = tip + new Vector2(1, -angle) * 64; |
|
colors[2] = colors[5] = new Color(color, 0.15F); |
|
points[2] = tip + new Vector2(1, angle) * EffectiveRange; |
|
points[5] = tip + new Vector2(1, -angle) * EffectiveRange; |
|
colors[3] = colors[4] = new Color(color, 0.0F); |
|
points[3] = tip + new Vector2(1, angle) * MaximumRange; |
|
points[4] = tip + new Vector2(1, -angle) * MaximumRange; |
|
|
|
var st = new SurfaceTool(); |
|
st.Begin(Mesh.PrimitiveType.TriangleStrip); |
|
st.AddColor(colors[0]); |
|
st.AddVertex(To3(points[0])); |
|
st.AddColor(colors[1]); |
|
st.AddVertex(To3(points[1])); |
|
st.AddVertex(To3(points[6])); |
|
st.AddColor(colors[2]); |
|
st.AddVertex(To3(points[2])); |
|
st.AddVertex(To3(points[5])); |
|
st.AddColor(colors[3]); |
|
st.AddVertex(To3(points[3])); |
|
st.AddVertex(To3(points[4])); |
|
st.Index(); |
|
|
|
DrawMesh(st.Commit(), null); |
|
DrawPolylineColors(points, colors, antialiased: true); |
|
} |
|
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) || (_fireDelay > 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. |
|
_fireDelay += 60.0F / RateOfFire; |
|
Rounds -= 1; |
|
} |
|
return true; |
|
} |
|
}
|
|
|