feat: add plasma gun weapon implementation and new pause menu system with loadout management

This commit is contained in:
DottsGit
2026-06-04 17:06:50 -04:00
parent 67bd5b4302
commit 1ccc66a601
37 changed files with 1070 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
extends BaseHitscanWeapon
class_name AK47
func _init() -> void:
weapon_name = "AK-47"
fire_rate = 0.15 # ~400 RPM
max_ammo = 30
reload_time = 2.0
base_damage = 35.0
min_damage = 25.0
falloff_start = 25.0
max_range = 150.0
automatic = true
spread_angle = 0.015
func _build_model() -> void:
# Position
position = Vector3(0.3, -0.3, -0.6)
# Receiver
var receiver = MeshInstance3D.new()
var rec_mesh = BoxMesh.new()
rec_mesh.size = Vector3(0.06, 0.12, 0.3)
var rec_mat = StandardMaterial3D.new()
rec_mat.albedo_color = Color(0.2, 0.2, 0.2)
rec_mesh.material = rec_mat
receiver.mesh = rec_mesh
receiver.position = Vector3(0, 0, 0.1)
model_root.add_child(receiver)
# Barrel
var barrel = MeshInstance3D.new()
var b_mesh = CylinderMesh.new()
b_mesh.top_radius = 0.015
b_mesh.bottom_radius = 0.015
b_mesh.height = 0.45
b_mesh.material = rec_mat
barrel.mesh = b_mesh
barrel.rotation.x = deg_to_rad(90)
barrel.position = Vector3(0, 0.03, -0.2)
model_root.add_child(barrel)
# Wood Handguard
var guard = MeshInstance3D.new()
var g_mesh = BoxMesh.new()
g_mesh.size = Vector3(0.08, 0.08, 0.2)
var wood_mat = StandardMaterial3D.new()
wood_mat.albedo_color = Color(0.5, 0.25, 0.1)
g_mesh.material = wood_mat
guard.mesh = g_mesh
guard.position = Vector3(0, 0, -0.1)
model_root.add_child(guard)
# Wood Stock
var stock = MeshInstance3D.new()
var s_mesh = BoxMesh.new()
s_mesh.size = Vector3(0.06, 0.1, 0.25)
s_mesh.material = wood_mat
stock.mesh = s_mesh
stock.position = Vector3(0, -0.02, 0.35)
model_root.add_child(stock)
# Muzzle Flash
muzzle_flash = OmniLight3D.new()
muzzle_flash.light_color = Color(1.0, 0.7, 0.2)
muzzle_flash.light_energy = 0.0
muzzle_flash.omni_range = 5.0
muzzle_flash.position = Vector3(0, 0.03, -0.45)
model_root.add_child(muzzle_flash)
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.stream = load("res://assets/sounds/ak47_fire.wav")
fire_sound.position = muzzle_flash.position
fire_sound.volume_db = -5.0
model_root.add_child(fire_sound)
reload_sound = AudioStreamPlayer3D.new()
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # generic reload
model_root.add_child(reload_sound)
+1
View File
@@ -0,0 +1 @@
uid://bddm0nooqfj2
+138
View File
@@ -0,0 +1,138 @@
extends Node3D
class_name BaseHitscanWeapon
@export var weapon_name: String = "Base Hitscan"
@export var fire_rate: float = 0.1 # Seconds between shots
@export var max_ammo: int = 30
@export var reload_time: float = 1.5
@export var base_damage: float = 10.0
@export var min_damage: float = 5.0
@export var falloff_start: float = 10.0
@export var max_range: float = 100.0
@export var automatic: bool = true
@export var spread_angle: float = 0.01
var current_ammo: int = 30
var reloading: bool = false
var reload_timer: float = 0.0
var fire_cooldown: float = 0.0
var is_firing: bool = false
var player: CharacterBody3D
var camera: Camera3D
var muzzle_flash: OmniLight3D
var fire_sound: AudioStreamPlayer3D
var reload_sound: AudioStreamPlayer3D
var model_root: Node3D
func _ready() -> void:
current_ammo = max_ammo
set_process_input(true)
set_process(true)
model_root = Node3D.new()
add_child(model_root)
_build_model()
func _build_model() -> void:
# Virtual method to be overridden
pass
func _process(delta: float) -> void:
if fire_cooldown > 0.0:
fire_cooldown -= delta
if reloading:
reload_timer -= delta
var spin_speed = (PI * 2.0) / reload_time
model_root.rotation.x += spin_speed * delta
if reload_timer <= 0.0:
current_ammo = max_ammo
reloading = false
model_root.rotation.x = 0.0
print(weapon_name, " Reloaded!")
elif automatic and is_firing and fire_cooldown <= 0.0 and current_ammo > 0:
_fire()
func _input(event: InputEvent) -> void:
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
return
if event.is_action_pressed("fire"):
is_firing = true
if not automatic and fire_cooldown <= 0.0 and current_ammo > 0 and not reloading:
_fire()
elif current_ammo == 0 and not reloading:
_start_reload()
elif event.is_action_released("fire"):
is_firing = false
if event.is_action_pressed("reload"):
if current_ammo < max_ammo and not reloading:
_start_reload()
func _start_reload() -> void:
reloading = true
reload_timer = reload_time
is_firing = false
if reload_sound:
reload_sound.play()
func _fire() -> void:
if current_ammo <= 0 or reloading:
return
current_ammo -= 1
fire_cooldown = fire_rate
_shoot_hitscan()
_play_muzzle_flash()
if fire_sound:
# Slight pitch variation
fire_sound.pitch_scale = 1.0 + randf_range(-0.05, 0.05)
fire_sound.play()
if current_ammo == 0:
_start_reload()
func _play_muzzle_flash() -> void:
if muzzle_flash:
muzzle_flash.light_energy = 8.0
var tween = create_tween()
tween.tween_property(muzzle_flash, "light_energy", 0.0, 0.05)
func _shoot_hitscan() -> void:
if not camera: return
var space_state = camera.get_world_3d().direct_space_state
var origin = camera.global_position
var forward = -camera.global_transform.basis.z.normalized()
var right = camera.global_transform.basis.x.normalized()
var up = camera.global_transform.basis.y.normalized()
# Calculate spread
var r = sqrt(randf()) * spread_angle
var theta = randf() * PI * 2.0
var offset_dir = (right * cos(theta) + up * sin(theta)) * r
var ray_dir = (forward + offset_dir).normalized()
var target = origin + ray_dir * max_range
var query = PhysicsRayQueryParameters3D.create(origin, target)
if player:
query.exclude = [player.get_rid()]
var result = space_state.intersect_ray(query)
if result:
var hit_dist = origin.distance_to(result.position)
var damage = base_damage
if hit_dist > falloff_start:
var falloff_factor = clampf((hit_dist - falloff_start) / (max_range - falloff_start), 0.0, 1.0)
damage = lerpf(base_damage, min_damage, falloff_factor)
if result.collider.has_method("take_damage"):
result.collider.take_damage(damage, result.position, player)
+1
View File
@@ -0,0 +1 @@
uid://l06bdsfbd0up
+126
View File
@@ -0,0 +1,126 @@
extends Node3D
class_name BaseProjectileWeapon
@export var weapon_name: String = "Base Projectile"
@export var fire_rate: float = 0.1
@export var max_ammo: int = 30
@export var reload_time: float = 1.5
@export var base_damage: float = 10.0
@export var min_damage: float = 5.0
@export var falloff_start: float = 10.0
@export var max_range: float = 200.0
@export var automatic: bool = true
@export var spread_angle: float = 0.01
@export var projectile_speed: float = 50.0
@export var projectile_gravity: float = 0.0
var current_ammo: int = 30
var reloading: bool = false
var reload_timer: float = 0.0
var fire_cooldown: float = 0.0
var is_firing: bool = false
var player: CharacterBody3D
var camera: Camera3D
var muzzle_flash: OmniLight3D
var fire_sound: AudioStreamPlayer3D
var reload_sound: AudioStreamPlayer3D
var model_root: Node3D
func _ready() -> void:
current_ammo = max_ammo
set_process_input(true)
set_process(true)
model_root = Node3D.new()
add_child(model_root)
_build_model()
func _build_model() -> void:
pass
func _process(delta: float) -> void:
if fire_cooldown > 0.0:
fire_cooldown -= delta
if reloading:
reload_timer -= delta
var spin_speed = (PI * 2.0) / reload_time
model_root.rotation.x += spin_speed * delta
if reload_timer <= 0.0:
current_ammo = max_ammo
reloading = false
model_root.rotation.x = 0.0
print(weapon_name, " Reloaded!")
elif automatic and is_firing and fire_cooldown <= 0.0 and current_ammo > 0:
_fire()
func _input(event: InputEvent) -> void:
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
return
if event.is_action_pressed("fire"):
is_firing = true
if not automatic and fire_cooldown <= 0.0 and current_ammo > 0 and not reloading:
_fire()
elif current_ammo == 0 and not reloading:
_start_reload()
elif event.is_action_released("fire"):
is_firing = false
if event.is_action_pressed("reload"):
if current_ammo < max_ammo and not reloading:
_start_reload()
func _start_reload() -> void:
reloading = true
reload_timer = reload_time
is_firing = false
if reload_sound:
reload_sound.play()
func _fire() -> void:
if current_ammo <= 0 or reloading:
return
current_ammo -= 1
fire_cooldown = fire_rate
_shoot_projectile()
_play_muzzle_flash()
if fire_sound:
fire_sound.pitch_scale = 1.0 + randf_range(-0.05, 0.05)
fire_sound.play()
if current_ammo == 0:
_start_reload()
func _play_muzzle_flash() -> void:
if muzzle_flash:
muzzle_flash.light_energy = 8.0
var tween = create_tween()
tween.tween_property(muzzle_flash, "light_energy", 0.0, 0.05)
func _shoot_projectile() -> void:
if not camera: return
var origin = camera.global_position
var forward = -camera.global_transform.basis.z.normalized()
var right = camera.global_transform.basis.x.normalized()
var up = camera.global_transform.basis.y.normalized()
# Calculate spread
var r = sqrt(randf()) * spread_angle
var theta = randf() * PI * 2.0
var offset_dir = (right * cos(theta) + up * sin(theta)) * r
var fire_dir = (forward + offset_dir).normalized()
_spawn_custom_projectile(origin, fire_dir)
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
# Overridden by specific weapons to instance their unique visual projectile
pass
+1
View File
@@ -0,0 +1 @@
uid://cbkr1yr1xtfum
+112
View File
@@ -0,0 +1,112 @@
extends BaseHitscanWeapon
class_name DMR
var is_ads: bool = false
var default_fov: float = 90.0
var ads_fov: float = 40.0
func _init() -> void:
weapon_name = "DMR"
fire_rate = 0.3 # Cooldown to prevent autoclicker spam
max_ammo = 15
reload_time = 2.2
base_damage = 75.0
min_damage = 60.0
falloff_start = 50.0
max_range = 300.0
automatic = false
spread_angle = 0.002 # Extremely accurate
func _ready() -> void:
super._ready()
if camera:
default_fov = camera.fov
func _build_model() -> void:
position = Vector3(0.3, -0.3, -0.7)
var body_mat = StandardMaterial3D.new()
body_mat.albedo_color = Color(0.15, 0.25, 0.15) # Olive drab
var black_mat = StandardMaterial3D.new()
black_mat.albedo_color = Color(0.05, 0.05, 0.05)
# Receiver
var receiver = MeshInstance3D.new()
var rec_mesh = BoxMesh.new()
rec_mesh.size = Vector3(0.05, 0.12, 0.4)
rec_mesh.material = body_mat
receiver.mesh = rec_mesh
receiver.position = Vector3(0, 0, 0.1)
model_root.add_child(receiver)
# Barrel
var barrel = MeshInstance3D.new()
var b_mesh = CylinderMesh.new()
b_mesh.top_radius = 0.01
b_mesh.bottom_radius = 0.01
b_mesh.height = 0.6
b_mesh.material = black_mat
barrel.mesh = b_mesh
barrel.rotation.x = deg_to_rad(90)
barrel.position = Vector3(0, 0.03, -0.3)
model_root.add_child(barrel)
# Scope
var scope = MeshInstance3D.new()
var scope_mesh = CylinderMesh.new()
scope_mesh.top_radius = 0.02
scope_mesh.bottom_radius = 0.02
scope_mesh.height = 0.2
scope_mesh.material = black_mat
scope.mesh = scope_mesh
scope.rotation.x = deg_to_rad(90)
scope.position = Vector3(0, 0.08, 0.05)
model_root.add_child(scope)
# Muzzle Flash
muzzle_flash = OmniLight3D.new()
muzzle_flash.light_color = Color(1.0, 0.8, 0.4)
muzzle_flash.light_energy = 0.0
muzzle_flash.omni_range = 6.0
muzzle_flash.position = Vector3(0, 0.03, -0.6)
model_root.add_child(muzzle_flash)
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.stream = load("res://assets/sounds/dmr_fire.wav")
fire_sound.position = muzzle_flash.position
fire_sound.volume_db = -3.0
model_root.add_child(fire_sound)
reload_sound = AudioStreamPlayer3D.new()
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
func _process(delta: float) -> void:
super._process(delta)
if camera:
var target_fov = ads_fov if is_ads else default_fov
camera.fov = lerpf(camera.fov, target_fov, 15.0 * delta)
# Move model to center when ADS
var target_pos = Vector3(0.0, -0.2, -0.5) if is_ads else Vector3(0.3, -0.3, -0.7)
position = position.lerp(target_pos, 15.0 * delta)
# Adjust sensitivity if player has a way to modify it (we can modify the camera rig later)
# For now, FOV alone gives the zoom effect.
func _input(event: InputEvent) -> void:
super._input(event)
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
return
if event.is_action_pressed("fire_alt"):
is_ads = true
elif event.is_action_released("fire_alt"):
is_ads = false
func _start_reload() -> void:
super._start_reload()
is_ads = false # Drop out of ADS on reload
+1
View File
@@ -0,0 +1 @@
uid://dy2xsq04e6ryt
+71
View File
@@ -0,0 +1,71 @@
extends BaseHitscanWeapon
class_name M4
func _init() -> void:
weapon_name = "M4"
fire_rate = 0.08 # ~750 RPM
max_ammo = 40
reload_time = 1.6
base_damage = 22.0
min_damage = 12.0
falloff_start = 15.0
max_range = 100.0
automatic = true
spread_angle = 0.01
func _build_model() -> void:
position = Vector3(0.3, -0.3, -0.6)
var grey_mat = StandardMaterial3D.new()
grey_mat.albedo_color = Color(0.25, 0.25, 0.25)
var dark_mat = StandardMaterial3D.new()
dark_mat.albedo_color = Color(0.1, 0.1, 0.1)
# Receiver
var receiver = MeshInstance3D.new()
var rec_mesh = BoxMesh.new()
rec_mesh.size = Vector3(0.05, 0.14, 0.25)
rec_mesh.material = grey_mat
receiver.mesh = rec_mesh
receiver.position = Vector3(0, 0, 0.1)
model_root.add_child(receiver)
# Barrel
var barrel = MeshInstance3D.new()
var b_mesh = CylinderMesh.new()
b_mesh.top_radius = 0.012
b_mesh.bottom_radius = 0.012
b_mesh.height = 0.4
b_mesh.material = dark_mat
barrel.mesh = b_mesh
barrel.rotation.x = deg_to_rad(90)
barrel.position = Vector3(0, 0.04, -0.2)
model_root.add_child(barrel)
# Handguard
var guard = MeshInstance3D.new()
var g_mesh = BoxMesh.new()
g_mesh.size = Vector3(0.06, 0.07, 0.22)
g_mesh.material = dark_mat
guard.mesh = g_mesh
guard.position = Vector3(0, 0.04, -0.1)
model_root.add_child(guard)
# Muzzle Flash
muzzle_flash = OmniLight3D.new()
muzzle_flash.light_color = Color(1.0, 0.8, 0.4)
muzzle_flash.light_energy = 0.0
muzzle_flash.omni_range = 4.0
muzzle_flash.position = Vector3(0, 0.04, -0.4)
model_root.add_child(muzzle_flash)
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.stream = load("res://assets/sounds/m4_fire.wav")
fire_sound.position = muzzle_flash.position
fire_sound.volume_db = -8.0
model_root.add_child(fire_sound)
reload_sound = AudioStreamPlayer3D.new()
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
+1
View File
@@ -0,0 +1 @@
uid://bctti6s7em04f
+80
View File
@@ -0,0 +1,80 @@
extends BaseHitscanWeapon
class_name MP7
func _init() -> void:
weapon_name = "MP7"
fire_rate = 0.05 # ~1200 RPM
max_ammo = 45
reload_time = 1.2
base_damage = 12.0
min_damage = 3.0
falloff_start = 8.0
max_range = 50.0
automatic = true
spread_angle = 0.025
func _build_model() -> void:
position = Vector3(0.25, -0.25, -0.5)
var black_mat = StandardMaterial3D.new()
black_mat.albedo_color = Color(0.05, 0.05, 0.05)
# Receiver
var receiver = MeshInstance3D.new()
var rec_mesh = BoxMesh.new()
rec_mesh.size = Vector3(0.04, 0.1, 0.2)
rec_mesh.material = black_mat
receiver.mesh = rec_mesh
receiver.position = Vector3(0, 0, 0)
model_root.add_child(receiver)
# Grip
var grip = MeshInstance3D.new()
var g_mesh = BoxMesh.new()
g_mesh.size = Vector3(0.03, 0.12, 0.04)
g_mesh.material = black_mat
grip.mesh = g_mesh
grip.position = Vector3(0, -0.06, 0.08)
grip.rotation.x = deg_to_rad(10)
model_root.add_child(grip)
# Foregrip
var fgrip = MeshInstance3D.new()
var fg_mesh = BoxMesh.new()
fg_mesh.size = Vector3(0.03, 0.08, 0.03)
fg_mesh.material = black_mat
fgrip.mesh = fg_mesh
fgrip.position = Vector3(0, -0.05, -0.06)
fgrip.rotation.x = deg_to_rad(-10)
model_root.add_child(fgrip)
# Barrel
var barrel = MeshInstance3D.new()
var b_mesh = CylinderMesh.new()
b_mesh.top_radius = 0.008
b_mesh.bottom_radius = 0.008
b_mesh.height = 0.1
b_mesh.material = black_mat
barrel.mesh = b_mesh
barrel.rotation.x = deg_to_rad(90)
barrel.position = Vector3(0, 0.02, -0.15)
model_root.add_child(barrel)
# Muzzle Flash
muzzle_flash = OmniLight3D.new()
muzzle_flash.light_color = Color(1.0, 0.9, 0.5)
muzzle_flash.light_energy = 0.0
muzzle_flash.omni_range = 3.0
muzzle_flash.position = Vector3(0, 0.02, -0.2)
model_root.add_child(muzzle_flash)
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.stream = load("res://assets/sounds/mp7_fire.wav")
fire_sound.position = muzzle_flash.position
fire_sound.volume_db = -10.0
model_root.add_child(fire_sound)
reload_sound = AudioStreamPlayer3D.new()
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
+1
View File
@@ -0,0 +1 @@
uid://unto0q3j6jix
+124
View File
@@ -0,0 +1,124 @@
extends BaseProjectileWeapon
class_name NailGun
func _init() -> void:
weapon_name = "Nail Gun"
fire_rate = 0.06 # Very fast (1000 RPM)
max_ammo = 60
reload_time = 2.0
base_damage = 18.0
min_damage = 10.0
falloff_start = 20.0
max_range = 100.0
automatic = true
spread_angle = 0.02
projectile_speed = 60.0 # Fast but requires lead
projectile_gravity = 5.0 # Slight drop
func _build_model() -> void:
position = Vector3(0.3, -0.3, -0.6)
var yellow_mat = StandardMaterial3D.new()
yellow_mat.albedo_color = Color(0.8, 0.7, 0.1) # Construction yellow
var dark_mat = StandardMaterial3D.new()
dark_mat.albedo_color = Color(0.2, 0.2, 0.2)
# Body
var body = MeshInstance3D.new()
var b_mesh = BoxMesh.new()
b_mesh.size = Vector3(0.06, 0.16, 0.25)
b_mesh.material = yellow_mat
body.mesh = b_mesh
body.position = Vector3(0, 0.02, 0)
model_root.add_child(body)
# Handle
var handle = MeshInstance3D.new()
var h_mesh = BoxMesh.new()
h_mesh.size = Vector3(0.04, 0.1, 0.04)
h_mesh.material = dark_mat
handle.mesh = h_mesh
handle.position = Vector3(0, -0.1, 0.05)
model_root.add_child(handle)
# Mag (Nail Drum)
var drum = MeshInstance3D.new()
var d_mesh = CylinderMesh.new()
d_mesh.top_radius = 0.05
d_mesh.bottom_radius = 0.05
d_mesh.height = 0.1
d_mesh.material = dark_mat
drum.mesh = d_mesh
drum.rotation.z = deg_to_rad(90)
drum.position = Vector3(0, -0.06, -0.05)
model_root.add_child(drum)
# Barrel
var barrel = MeshInstance3D.new()
var bar_mesh = CylinderMesh.new()
bar_mesh.top_radius = 0.01
bar_mesh.bottom_radius = 0.01
bar_mesh.height = 0.15
bar_mesh.material = dark_mat
barrel.mesh = bar_mesh
barrel.rotation.x = deg_to_rad(90)
barrel.position = Vector3(0, 0.05, -0.2)
model_root.add_child(barrel)
# Muzzle Flash (Sparks)
muzzle_flash = OmniLight3D.new()
muzzle_flash.light_color = Color(1.0, 0.9, 0.6)
muzzle_flash.light_energy = 0.0
muzzle_flash.omni_range = 3.0
muzzle_flash.position = Vector3(0, 0.05, -0.3)
model_root.add_child(muzzle_flash)
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.stream = load("res://assets/sounds/nailgun_fire.wav")
fire_sound.position = muzzle_flash.position
fire_sound.volume_db = -5.0
model_root.add_child(fire_sound)
reload_sound = AudioStreamPlayer3D.new()
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
var proj = load("res://weapons/projectile.gd").new()
# Give it a nail/spike mesh
var mesh_instance = MeshInstance3D.new()
var p_mesh = CylinderMesh.new()
p_mesh.top_radius = 0.0
p_mesh.bottom_radius = 0.01
p_mesh.height = 0.1
var mat = StandardMaterial3D.new()
mat.albedo_color = Color(0.7, 0.7, 0.7) # Shiny metal
mat.metallic = 1.0
mat.roughness = 0.3
p_mesh.material = mat
mesh_instance.mesh = p_mesh
# Rotate it so it points forward
mesh_instance.rotation.x = deg_to_rad(-90)
proj.add_child(mesh_instance)
# Projectile rotation so it faces velocity
# (Basis looking towards fire_dir)
var up = Vector3.UP
if abs(fire_dir.dot(up)) > 0.99:
up = Vector3.RIGHT
proj.transform.basis = Basis.looking_at(fire_dir, up)
proj.global_position = muzzle_flash.global_position if muzzle_flash else origin
proj.velocity = fire_dir * projectile_speed
proj.damage = base_damage
proj.min_damage = min_damage
proj.falloff_start = falloff_start
proj.max_range = max_range
proj.drop_gravity = projectile_gravity
proj.owner_player = player
# Spawn in the world
get_tree().current_scene.add_child(proj)
+1
View File
@@ -0,0 +1 @@
uid://bkl7mdau5363
+103
View File
@@ -0,0 +1,103 @@
extends BaseProjectileWeapon
class_name PlasmaGun
func _init() -> void:
weapon_name = "Plasma Gun"
fire_rate = 0.15 # Moderate
max_ammo = 25
reload_time = 1.8
base_damage = 50.0
min_damage = 50.0 # No falloff
falloff_start = 0.0 # Unused because min=base
max_range = 300.0
automatic = true
spread_angle = 0.005 # Highly accurate
projectile_speed = 80.0
projectile_gravity = 0.0
func _build_model() -> void:
position = Vector3(0.3, -0.3, -0.6)
var white_mat = StandardMaterial3D.new()
white_mat.albedo_color = Color(0.8, 0.8, 0.9)
var glow_mat = StandardMaterial3D.new()
glow_mat.albedo_color = Color(0.2, 0.5, 1.0)
glow_mat.emission_enabled = true
glow_mat.emission = Color(0.2, 0.5, 1.0)
glow_mat.emission_energy_multiplier = 4.0
# Body
var body = MeshInstance3D.new()
var b_mesh = BoxMesh.new()
b_mesh.size = Vector3(0.08, 0.14, 0.35)
b_mesh.material = white_mat
body.mesh = b_mesh
body.position = Vector3(0, 0, 0.05)
model_root.add_child(body)
# Glow Core
var core = MeshInstance3D.new()
var c_mesh = CylinderMesh.new()
c_mesh.top_radius = 0.02
c_mesh.bottom_radius = 0.02
c_mesh.height = 0.4
c_mesh.material = glow_mat
core.mesh = c_mesh
core.rotation.x = deg_to_rad(90)
core.position = Vector3(0, 0.02, -0.15)
model_root.add_child(core)
# Muzzle Flash
muzzle_flash = OmniLight3D.new()
muzzle_flash.light_color = Color(0.2, 0.5, 1.0)
muzzle_flash.light_energy = 0.0
muzzle_flash.omni_range = 6.0
muzzle_flash.position = Vector3(0, 0.02, -0.4)
model_root.add_child(muzzle_flash)
# Audio
fire_sound = AudioStreamPlayer3D.new()
fire_sound.stream = load("res://assets/sounds/plasma_fire.wav")
fire_sound.position = muzzle_flash.position
fire_sound.volume_db = -5.0
model_root.add_child(fire_sound)
reload_sound = AudioStreamPlayer3D.new()
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
model_root.add_child(reload_sound)
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
var proj = load("res://weapons/projectile.gd").new()
# Give it a glowing blue mesh
var mesh_instance = MeshInstance3D.new()
var p_mesh = SphereMesh.new()
p_mesh.radius = 0.08
p_mesh.height = 0.16
var mat = StandardMaterial3D.new()
mat.albedo_color = Color(0.2, 0.6, 1.0)
mat.emission_enabled = true
mat.emission = Color(0.2, 0.6, 1.0)
mat.emission_energy_multiplier = 5.0
p_mesh.material = mat
mesh_instance.mesh = p_mesh
proj.add_child(mesh_instance)
var light = OmniLight3D.new()
light.light_color = Color(0.2, 0.6, 1.0)
light.light_energy = 2.0
light.omni_range = 4.0
proj.add_child(light)
proj.global_position = muzzle_flash.global_position if muzzle_flash else origin
proj.velocity = fire_dir * projectile_speed
proj.damage = base_damage
proj.min_damage = min_damage
proj.falloff_start = falloff_start
proj.max_range = max_range
proj.drop_gravity = projectile_gravity
proj.owner_player = player
# Spawn in the world
get_tree().current_scene.add_child(proj)
+1
View File
@@ -0,0 +1 @@
uid://n156sg54wvpp
+59
View File
@@ -0,0 +1,59 @@
extends Node3D
class_name Projectile
var velocity: Vector3 = Vector3.ZERO
var damage: float = 10.0
var max_range: float = 200.0
var distance_traveled: float = 0.0
var owner_player: CharacterBody3D
var falloff_start: float = 10.0
var min_damage: float = 5.0
var drop_gravity: float = 0.0 # For arc projectiles (nailgun might have slight drop, plasma none)
var _previous_position: Vector3
func _ready() -> void:
_previous_position = global_position
# Automatically destroy after 5 seconds to prevent leaks
var t = get_tree().create_timer(5.0)
t.timeout.connect(queue_free)
func _physics_process(delta: float) -> void:
# Apply gravity drop if any
velocity.y -= drop_gravity * delta
var movement = velocity * delta
global_position += movement
distance_traveled += movement.length()
if distance_traveled > max_range:
queue_free()
return
_check_collision()
_previous_position = global_position
func _check_collision() -> void:
var space_state = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(_previous_position, global_position)
if owner_player:
query.exclude = [owner_player.get_rid()]
var result = space_state.intersect_ray(query)
if result:
_on_hit(result)
func _on_hit(result: Dictionary) -> void:
var hit_dist = distance_traveled - (_previous_position.distance_to(result.position))
var final_damage = damage
if hit_dist > falloff_start and falloff_start > 0.0:
var falloff_factor = clampf((hit_dist - falloff_start) / (max_range - falloff_start), 0.0, 1.0)
final_damage = lerpf(damage, min_damage, falloff_factor)
if result.collider.has_method("take_damage"):
result.collider.take_damage(final_damage, result.position, owner_player)
# Spawn impact effect? Not needed yet for prototype.
queue_free()
+1
View File
@@ -0,0 +1 @@
uid://davfge7rifxls
+4
View File
@@ -55,6 +55,10 @@ func _equip_slot(slot: int) -> void:
var w = weapons[active_slot]
w.visible = false
w.set_process_input(false)
if "is_firing" in w:
w.is_firing = false
if w.has_method("unequip"):
w.unequip()
active_slot = slot