2D multiplayer platformer using Godot Engine
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.
 

272 lines
8.9 KiB

# TODO: "Click" sound when attempting to fire when not ready, or empty.
# TODO: "Single reload" for revolver & shotgun.
# TODO: Add outline around weapon sprites.
class_name Weapon
extends Item
const NETWORK_EPSILON := 0.05
@export var automatic := false
@export var fire_rate := 100 # rounds/minute
@export var capacity := 12
@export var reload_time := 1.0
@export var knockback := 0.0
@export var spread := 0.0
@export var spread_increase := 0.0
@export var recoil_min := 0.0
@export var recoil_max := 0.0
@export var effective_range := 320
@export var maximum_range := 640
@export var bullet_velocity := 2000
@export var bullets_per_shot := 1
@export var damage := 0.0
@export var bullet_opacity := 0.2
@onready var rounds := capacity
@onready var tip_offset := $Tip.position as Vector2
var is_reloading: bool:
get: return _reload_delay > 0.0
var reload_progress: float:
get: return clampf(1 - _reload_delay / reload_time, 0.0, 1.0)
var lowered := false
var aim_direction: float
# TODO: Tell the server when we're pressing/releasing the trigger.
var _trigger_held := -INF
var _fire_delay := 0.0
var _reload_delay := 0.0
var _current_spread_inc := 0.0
var _current_recoil := 0.0
# Needs to be kept alive for `draw_mesh` to work.
var _mesh: ArrayMesh
func _unhandled_input(event: InputEvent) -> void:
if not player.network.is_local: return
if not is_equipped: return
if event.is_action_pressed("interact_primary"):
_trigger_held = 0.0
_on_trigger_pressed()
if event.is_action_pressed("interact_reload"):
reload()
func _on_trigger_pressed() -> void:
fire()
func _on_trigger_released() -> void:
pass
func _process(delta: float) -> void:
var speed_decrease := maxf(TAU / 200, _current_spread_inc * 1.5)
var recoil_decrease := maxf(TAU / 600, _current_recoil * 1.5)
_current_spread_inc = maxf(0.0, _current_spread_inc - speed_decrease * delta)
_current_recoil = maxf(0.0, _current_recoil - recoil_decrease * delta)
if not player.health.is_alive:
# TODO: Do this once when player respawns.
rounds = capacity
_trigger_held = -INF
_fire_delay = 0.0
_reload_delay = 0.0
if player.network.is_local: queue_redraw()
elif not is_equipped:
# When switching away from the weapon while reloading, reset reload time.
if is_reloading: _reload_delay = reload_time
else:
if _trigger_held >= 0.0:
_trigger_held += delta
if is_reloading:
_reload_delay -= delta
if _reload_delay <= 0.0:
_reload_delay = 0.0
rounds = capacity
if _fire_delay > 0.0:
_fire_delay -= 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 _fire_delay < 0.0 and (!automatic || _trigger_held < 0.0):
_fire_delay = 0.0
if player.network.is_local:
# Automatically reload when out of rounds.
if rounds <= 0: reload()
if _trigger_held >= 0.0:
if !Input.is_action_pressed("interact_primary"):
_trigger_held = -INF
_on_trigger_released()
elif automatic:
fire()
# Gun tip_offset C = Cursor
# v v b v
# x---###########x------------------------------x
# | ##==# _____-----
# a | ## _____-----
# | _____----- c
# x_____-----
# ^
# B = Player
# The length of `a` and `c` as well as the angle of `c` are known.
# `a` is the y component of the weapon's TipOffset.
# `c` is the line connecting the player and cursor.
# Find out the angle `C` to subtract from the already known angle of `c`.
# CREDIT to lizzie for helping me figure out this trigonometry problem.
var a := tip_offset.y * (1 if (scale.y > 0) else -1)
var c := player.position.distance_to(cursor.position)
# If the cursor is too close to the player, put the
# weapon in a "lowered" state, where it can't be shot.
lowered = c < tip_offset.x
if lowered: aim_direction = deg_to_rad(30 if cursor.position.x > player.position.x else 150)
else: aim_direction = player.position.angle_to_point(cursor.position) - asin(a / c)
# TODO: Send aim angle
queue_redraw()
else:
_reload_delay = 0.0
var angle = absf(rad_to_deg(fposmod(aim_direction + PI, TAU) - PI))
if scale.y > 0 and angle > 100: scale.y = -1
if scale.y < 0 and angle < 80: scale.y = 1
rotation = aim_direction - _current_recoil * (1 if scale.y > 0 else -1)
# [RPC(MultiplayerAPI.RPCMode.AnyPeer)]
# private void SendAimAngle(float value)
# {
# if (this.GetGame() is Server) {
# if ((Player.NetworkID != GetTree().GetRemoteSenderId()) || !Player.IsAlive) return;
# if (float.IsNaN(value = Mathf.PosMod(value, Mathf.Tau))) return;
# RPC.Unreliable(SendAimAngle, value);
# } else if (!Player.IsLocal)
# AimDirection = value;
# }
func fire() -> void:
var _seed := randi()
if !_fire_internal(aim_direction, scale.y > 0, _seed): return
# RPC.Reliable(1, SendFire, AimDirection, Scale.y > 0, _seed);
player.velocity -= knockback * Vector2.from_angle(rotation)
func _fire_internal(aim: float, to_right: bool, _seed: int) -> bool:
var is_server := false
var epsilon := 0.0 if is_server else NETWORK_EPSILON
if !player.health.is_alive: return false
if !is_equipped or lowered or rounds <= 0: return false
if _reload_delay > epsilon or _fire_delay > epsilon: return false
if !is_server:
var stream: AudioStreamPlayer2D = get_node_or_null("Fire")
if stream: stream.play()
var random := RandomNumberGenerator.new()
random.seed = _seed
var angle := aim - _current_recoil * (1 if to_right else -1)
var tip := (tip_offset if to_right else tip_offset * Vector2(1, -1)).rotated(angle)
for i in bullets_per_shot:
var spr := (deg_to_rad(spread) + _current_spread_inc) * clampf(random.randfn(0.4), -1, 1)
var dir := Vector2.from_angle(angle + spr)
var color := Color(player.appearance.color, bullet_opacity)
var bullet := Bullet.new(
player.global_position + tip, dir, effective_range, maximum_range,
bullet_velocity, damage / bullets_per_shot, color)
Game.WORLD.add_child(bullet)
_current_spread_inc += deg_to_rad(spread_increase)
_current_recoil += deg_to_rad(random.randf_range(recoil_min, recoil_max))
if is_server or player.network.is_local:
# Do not keep track of fire rate or ammo for other players.
_fire_delay += 60.0 / fire_rate
rounds -= 1
return true
# [RPC(MultiplayerAPI.RPCMode.AnyPeer)]
# private void SendFire(float aimDirection, bool toRight, int seed)
# {
# if (this.GetGame() is Server) {
# if (Player.NetworkID != GetTree().GetRemoteSenderId()) return;
# if (float.IsNaN(aimDirection = Mathf.PosMod(aimDirection, Mathf.Tau))) return;
# // TODO: Only send to players who can see the full path of the bullet.
# if (FireInternal(aimDirection, toRight, seed))
# RPC.Reliable(SendFire, aimDirection, toRight, seed);
# } else if (!Player.IsLocal)
# FireInternal(aimDirection, toRight, seed);
# }
func reload() -> void:
if _reload_internal():
pass # send_reload
func _reload_internal() -> bool:
if !is_equipped or !player.health.is_alive or rounds >= capacity or is_reloading: return false
# TODO: Play reload sound.
_reload_delay += reload_time
return true
# [RPC(MultiplayerAPI.RPCMode.AnyPeer)]
# private void SendReload()
# {
# if (this.GetGame() is Server) {
# if (Player.NetworkID != GetTree().GetRemoteSenderId()) return;
# if (ReloadInternal()) RPC.Reliable(SendReload);
# } else if (!Player.IsLocal)
# ReloadInternal();
# }
func _draw() -> void:
if !player.network.is_local or !player.health.is_alive or lowered: return
# Draws an "aiming cone" to show where bullets might travel.
var tip := tip_offset + Vector2(4, 0)
var angle := sin((deg_to_rad(spread) + _current_spread_inc) / 2)
var color := Color.BLACK
var points: Array[Vector2]
var colors: Array[Color]
var append := func(opacity: float, dist: float) -> void:
points.push_front(tip + Vector2(1, angle) * dist)
points.push_back (tip + Vector2(1, -angle) * dist)
colors.push_front(Color(color, opacity))
colors.push_back (Color(color, opacity))
append.call(0.00, maximum_range)
append.call(0.15, effective_range)
append.call(0.15, 64)
append.call(0.00, 0)
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLE_STRIP)
st.set_color(colors[0])
st.add_vertex(Vector3(points[0].x, points[0].y, 0))
st.set_color(colors[1])
st.add_vertex(Vector3(points[1].x, points[1].y, 0))
st.add_vertex(Vector3(points[6].x, points[6].y, 0))
st.set_color(colors[2])
st.add_vertex(Vector3(points[2].x, points[2].y, 0))
st.add_vertex(Vector3(points[5].x, points[5].y, 0))
st.set_color(colors[3])
st.add_vertex(Vector3(points[3].x, points[3].y, 0))
st.add_vertex(Vector3(points[4].x, points[5].y, 0))
_mesh = st.commit()
draw_mesh(_mesh, null)
draw_polyline_colors(points, colors, 1.0, true)