diff --git a/project.godot b/project.godot index 37512d5..00b7bd7 100644 --- a/project.godot +++ b/project.godot @@ -86,6 +86,11 @@ interact_secondary={ "events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":2,"pressed":false,"doubleclick":false,"script":null) ] } +interact_reload={ +"deadzone": 0.5, +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":82,"unicode":0,"echo":false,"script":null) + ] +} interact_select={ "deadzone": 0.5, "events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":3,"pressed":false,"doubleclick":false,"script":null) diff --git a/scene/ClientScene.tscn b/scene/ClientScene.tscn index f7c9650..9f2e3d5 100644 --- a/scene/ClientScene.tscn +++ b/scene/ClientScene.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=12 format=2] +[gd_scene load_steps=16 format=2] [ext_resource path="res://scene/GameScene.tscn" type="PackedScene" id=1] [ext_resource path="res://src/Cursor.cs" type="Script" id=2] @@ -11,6 +11,31 @@ [ext_resource path="res://src/Network/IntegratedServer.cs" type="Script" id=9] [ext_resource path="res://ui_theme.tres" type="Theme" id=10] [ext_resource path="res://src/RadialMenu.cs" type="Script" id=11] +[ext_resource path="res://src/WeaponInfo.cs" type="Script" id=12] + +[sub_resource type="StyleBoxFlat" id=1] +bg_color = Color( 0, 0, 0, 0.752941 ) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color( 0, 0, 0, 0.501961 ) +corner_detail = 1 +expand_margin_left = 1.0 +expand_margin_right = 1.0 +expand_margin_top = 1.0 +expand_margin_bottom = 1.0 + +[sub_resource type="StyleBoxFlat" id=2] +bg_color = Color( 0, 0.752941, 0, 0.752941 ) +corner_detail = 1 + +[sub_resource type="Theme" id=3] +ProgressBar/colors/font_color = Color( 0.94, 0.94, 0.94, 1 ) +ProgressBar/colors/font_color_shadow = Color( 0, 0, 0, 1 ) +ProgressBar/fonts/font = null +ProgressBar/styles/bg = SubResource( 1 ) +ProgressBar/styles/fg = SubResource( 2 ) [node name="Client" instance=ExtResource( 1 )] script = ExtResource( 8 ) @@ -70,3 +95,34 @@ follow_viewport_enable = true z_index = 1000 texture = ExtResource( 3 ) script = ExtResource( 2 ) + +[node name="WeaponInfo" type="Node2D" parent="CursorLayer/Cursor" index="0"] +script = ExtResource( 12 ) + +[node name="Rounds" type="Label" parent="CursorLayer/Cursor/WeaponInfo" index="0"] +anchor_left = 0.5 +anchor_right = 0.5 +margin_left = -40.0 +margin_top = 7.0 +margin_right = 40.0 +margin_bottom = 16.0 +theme = ExtResource( 10 ) +text = "12/12" +align = 1 +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Reloading" type="ProgressBar" parent="CursorLayer/Cursor/WeaponInfo" index="1"] +margin_left = -16.0 +margin_top = 10.0 +margin_right = 16.0 +margin_bottom = 13.0 +theme = SubResource( 3 ) +max_value = 1.0 +value = 0.5 +percent_visible = false +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/scene/Player.tscn b/scene/Player.tscn index 2099345..a2106cf 100644 --- a/scene/Player.tscn +++ b/scene/Player.tscn @@ -67,12 +67,15 @@ visible = false texture = ExtResource( 7 ) offset = Vector2( 8, 0 ) script = ExtResource( 8 ) +RateOfFire = 400 +Capacity = 6 +ReloadTime = 1.4 Knockback = 50.0 Spread = 1.5 SpreadIncrease = 1.0 RecoilMin = 3.0 RecoilMax = 5.0 -BulletSpeed = 1200 +BulletVelocity = 1200 [node name="Tip" type="Node2D" parent="Items/Revolver"] position = Vector2( 15, -2.5 ) @@ -85,15 +88,17 @@ visible = false texture = ExtResource( 9 ) offset = Vector2( 8, 0 ) script = ExtResource( 8 ) -EffectiveRange = 240 -MaximumRange = 360 +Capacity = 2 +ReloadTime = 2.0 Knockback = 135.0 Spread = 8.0 SpreadIncrease = 10.0 RecoilMin = 6.0 RecoilMax = 12.0 -BulletSpeed = 1000 -BulletsPetShot = 8 +EffectiveRange = 240 +MaximumRange = 360 +BulletVelocity = 1000 +BulletsPerShot = 6 BulletOpacity = 0.1 [node name="Tip" type="Node2D" parent="Items/Shotgun"] @@ -107,13 +112,15 @@ visible = false texture = ExtResource( 11 ) offset = Vector2( 8, 0 ) script = ExtResource( 8 ) -EffectiveRange = 480 -MaximumRange = 920 +Capacity = 1 +ReloadTime = 1.6 Knockback = 100.0 SpreadIncrease = 2.0 RecoilMin = 8.0 RecoilMax = 8.0 -BulletSpeed = 4000 +EffectiveRange = 480 +MaximumRange = 920 +BulletVelocity = 4000 BulletOpacity = 0.4 [node name="Tip" type="Node2D" parent="Items/Rifle"] @@ -127,11 +134,16 @@ visible = false texture = ExtResource( 12 ) offset = Vector2( 8, 0 ) script = ExtResource( 8 ) +Automatic = true +RateOfFire = 600 +Capacity = 30 +ReloadTime = 2.4 Knockback = 30.0 Spread = 0.6 SpreadIncrease = 0.8 RecoilMin = 1.0 RecoilMax = 2.5 +BulletOpacity = 0.15 [node name="Tip" type="Node2D" parent="Items/Assault Rifle"] position = Vector2( 22, -1.5 ) @@ -144,6 +156,7 @@ visible = false texture = ExtResource( 10 ) offset = Vector2( 8, 0 ) script = ExtResource( 8 ) +Automatic = true [node name="Tip" type="Node2D" parent="Items/Super Soaker"] position = Vector2( 17, 0.5 ) diff --git a/src/Items/Weapon.cs b/src/Items/Weapon.cs index d5af8e6..9a8f0d0 100644 --- a/src/Items/Weapon.cs +++ b/src/Items/Weapon.cs @@ -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; } = 320; - [Export] public int MaximumRange { get; set; } = 640; - [Export] public float Knockback { get; set; } = 0.0F; + [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 Spread { get; set; } = 0.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 float RecoilMin { get; set; } = 0.0F; - [Export] public float RecoilMax { get; set; } = 0.0F; - - [Export] public int BulletSpeed { get; set; } = 2000; - [Export] public int BulletsPetShot { get; set; } = 1; + [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 Cursor Cursor { get; private set; } - public Player Player { get; private set; } + public float _fireDelay; + public float? _reloading; private float _currentSpreadInc = 0.0F; - private float _currentRecoil = 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(); } 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); + 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; } } - private void ShootInternal(float aimDirection, bool toRight, int seed) - { - if (this.GetGame() is Client) - GetNodeOrNull("Fire")?.Play(); - - var random = new Random(seed); - var angle = aimDirection - _currentRecoil * (toRight ? 1 : -1); - - var tip = GetNode("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); - } - _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,10 +68,44 @@ public class Weapon : Sprite _currentSpreadInc = Mathf.Max(0, _currentSpreadInc - spreadDecrease * delta); _currentRecoil = Mathf.Max(0, _currentRecoil - recoilDecrease * delta); - if (Visible && (Player is LocalPlayer)) { - AimDirection = Cursor.Position.AngleToPoint(Player.Position); - RpcId(1, nameof(SendAimAngle), AimDirection); - Update(); + 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; @@ -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 _Draw() { if (!(Player is LocalPlayer)) return; + // Draws an "aiming cone" to show where bullets might travel. var tip = GetNode("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) || (_fireDelay > 0)) return false; + + if (this.GetGame() is Client) + GetNodeOrNull("Fire")?.Play(); + + var random = new Random(seed); + var angle = aimDirection - _currentRecoil * (toRight ? 1 : -1); + + var tip = GetNode("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; + } } diff --git a/src/Objects/Bullet.cs b/src/Objects/Bullet.cs index 20a30c1..a5c231c 100644 --- a/src/Objects/Bullet.cs +++ b/src/Objects/Bullet.cs @@ -8,7 +8,7 @@ public class Bullet : Node2D public Vector2 Direction { get; } public int EffectiveRange { get; } public int MaximumRange { get; } - public int Speed { get; } + public int Velocity { get; } public Color Color { get; } private readonly Vector2 _startPosition; @@ -16,14 +16,14 @@ public class Bullet : Node2D private float _distance; public Bullet(Vector2 position, Vector2 direction, - int effectiveRange, int maximumRange, int speed, Color color) + int effectiveRange, int maximumRange, int velocity, Color color) { _startPosition = Position = position; Direction = direction; EffectiveRange = effectiveRange; MaximumRange = maximumRange; - Speed = speed; - Color = color; + Velocity = velocity; + Color = color; } public override void _Ready() @@ -42,7 +42,7 @@ public class Bullet : Node2D public override void _PhysicsProcess(float delta) { var previousPosition = Position; - _distance = Mathf.Min(MaximumRange, Speed * (float)_age.TotalSeconds); + _distance = Mathf.Min(MaximumRange, Velocity * (float)_age.TotalSeconds); Position = _startPosition + Direction * _distance; var collision = GetWorld2d().DirectSpaceState.IntersectRay( diff --git a/src/RadialMenu.cs b/src/RadialMenu.cs index 1fb28e4..52c8732 100644 --- a/src/RadialMenu.cs +++ b/src/RadialMenu.cs @@ -1,6 +1,7 @@ using System; using Godot; +// TODO: Display number of rounds for weapons? Add an even smaller font for this? public class RadialMenu : Node2D { [Export] public int InnerRadius { get; set; } = 32; @@ -28,6 +29,8 @@ public class RadialMenu : Node2D public override void _UnhandledInput(InputEvent ev) { + if (GetItems() == null) return; + if (ev.IsActionPressed("interact_select")) { Position = this.GetClient().Cursor.ScreenPosition.Round(); Visible = true; @@ -41,6 +44,7 @@ public class RadialMenu : Node2D ActiveName.Text = _selected?.Name ?? ""; Update(); } + if (ev.IsActionPressed("interact_select_dec") || ev.IsActionPressed("interact_select_inc")) { var diff = ev.IsActionPressed("interact_select_inc") ? 1 : -1; var items = GetItems(); @@ -87,7 +91,9 @@ public class RadialMenu : Node2D return; } - var items = GetItems(); + var items = GetItems(); + if (items == null) return; + var cursor = ToLocal(this.GetClient().Cursor.ScreenPosition); var angle = cursor.Angle() - _startAngle; var index = (int)((angle / Mathf.Tau + 1) % 1 * MinElements); diff --git a/src/WeaponInfo.cs b/src/WeaponInfo.cs new file mode 100644 index 0000000..ad08668 --- /dev/null +++ b/src/WeaponInfo.cs @@ -0,0 +1,52 @@ +using System; +using Godot; + +// TODO: Render rounds as sprites? + +public class WeaponInfo : Node2D +{ + private static readonly TimeSpan VISIBLE_DURATION = TimeSpan.FromSeconds(0.8); + + public Label Rounds { get; private set; } + public ProgressBar Reloading { get; private set; } + + private float _visibleFor; + private Weapon _previousWeapon; + private int _previousRounds; + + public override void _Ready() + { + Rounds = GetNode