Feat/2 weapons foundation #6

Merged
Dotts merged 30 commits from feat/2-weapons-foundation into main 2026-06-04 22:22:16 -07:00
37 changed files with 1070 additions and 0 deletions
Showing only changes of commit 1ccc66a601 - Show all commits
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+123
View File
@@ -261,4 +261,127 @@ for i in range(int(sample_rate * dj_duration)):
dj_samples.append((filtered * 0.4 + lift) * env)
write_wav('assets/sounds/double_jump.wav', dj_samples)
# ==========================================
# NEW WEAPONS (AK47, M4, MP7, DMR, Plasma, Nail)
# ==========================================
# 11. AK47 (Heavy punch, low thud, sharp crack)
ak_duration = 0.2
ak_samples = []
for i in range(int(sample_rate * ak_duration)):
t = i / sample_rate
noise = random.uniform(-1.0, 1.0)
# Sharp metallic crack
crack_env = math.exp(-t * 100.0)
crack = noise * crack_env * 0.4
# Deep heavy punch (70Hz sweep down to 40Hz)
punch_freq = max(40.0, 70.0 - t * 300.0)
punch_env = math.exp(-t * 20.0)
punch = math.sin(t * punch_freq * 2 * math.pi) * punch_env * 0.6
# Mechanism ring
ring_env = math.exp(-t * 15.0)
ring = math.sin(t * 800.0 * 2 * math.pi) * ring_env * 0.05
ak_samples.append(crack + punch + ring)
write_wav('assets/sounds/ak47_fire.wav', ak_samples)
# 12. M4 (Moderate punch, sharper crack, tighter burst)
m4_duration = 0.15
m4_samples = []
for i in range(int(sample_rate * m4_duration)):
t = i / sample_rate
noise = random.uniform(-1.0, 1.0)
crack_env = math.exp(-t * 120.0)
crack = noise * crack_env * 0.5
punch_freq = 90.0
punch_env = math.exp(-t * 25.0)
punch = math.sin(t * punch_freq * 2 * math.pi) * punch_env * 0.4
m4_samples.append(crack + punch)
write_wav('assets/sounds/m4_fire.wav', m4_samples)
# 13. MP7 (Very fast, light pop, high pitched)
mp7_duration = 0.1
mp7_samples = []
for i in range(int(sample_rate * mp7_duration)):
t = i / sample_rate
noise = random.uniform(-1.0, 1.0)
pop_env = math.exp(-t * 80.0)
pop = noise * pop_env * 0.3
punch_freq = 150.0
punch_env = math.exp(-t * 35.0)
punch = math.sin(t * punch_freq * 2 * math.pi) * punch_env * 0.2
mp7_samples.append(pop + punch)
write_wav('assets/sounds/mp7_fire.wav', mp7_samples)
# 14. DMR (Extremely loud, echoing boom, very sharp)
dmr_duration = 0.4
dmr_samples = []
for i in range(int(sample_rate * dmr_duration)):
t = i / sample_rate
noise = random.uniform(-1.0, 1.0)
crack_env = math.exp(-t * 50.0)
crack = noise * crack_env * 0.6
boom_freq = max(30.0, 60.0 - t * 100.0)
boom_env = math.exp(-t * 8.0)
boom = math.sin(t * boom_freq * 2 * math.pi) * boom_env * 0.7
tail_env = math.exp(-t * 5.0)
tail = noise * tail_env * 0.1 # outdoor echo reflection
dmr_samples.append(crack + boom + tail)
write_wav('assets/sounds/dmr_fire.wav', dmr_samples)
# 15. Plasma Gun (Sci-Fi pew, tonal sweep)
plasma_duration = 0.2
plasma_samples = []
for i in range(int(sample_rate * plasma_duration)):
t = i / sample_rate
# Sweep from 1200 Hz down to 300 Hz rapidly
freq = max(300.0, 1200.0 * math.exp(-t * 30.0))
pew_env = math.exp(-t * 15.0)
pew = math.sin(t * freq * 2 * math.pi) * pew_env * 0.6
plasma_samples.append(pew)
write_wav('assets/sounds/plasma_fire.wav', plasma_samples)
# 16. Nail Gun (Clack clack clack, metallic hit)
nail_duration = 0.1
nail_samples = []
for i in range(int(sample_rate * nail_duration)):
t = i / sample_rate
noise = random.uniform(-1.0, 1.0)
# Sharp metallic impact
hit_env = math.exp(-t * 150.0)
hit = noise * hit_env * 0.4
# Ringing metal (two tones)
ring_env = math.exp(-t * 40.0)
ring = (math.sin(t * 3500.0 * 2 * math.pi) + math.sin(t * 4100.0 * 2 * math.pi)) * ring_env * 0.15
# Mechanism chunk
chunk_env = math.exp(-t * 30.0)
chunk = math.sin(t * 200.0 * 2 * math.pi) * chunk_env * 0.2
nail_samples.append(hit + ring + chunk)
write_wav('assets/sounds/nailgun_fire.wav', nail_samples)
print("Generated all sounds!")
+30
View File
@@ -12,6 +12,36 @@ var weapon_db = {
"name": "Double Barrel Shotgun",
"category": "primary",
"script": "res://weapons/double_barrel_shotgun.gd"
},
"ak47": {
"name": "AK-47",
"category": "primary",
"script": "res://weapons/ak47.gd"
},
"m4": {
"name": "M4",
"category": "primary",
"script": "res://weapons/m4.gd"
},
"mp7": {
"name": "MP7",
"category": "primary",
"script": "res://weapons/mp7.gd"
},
"dmr": {
"name": "DMR",
"category": "primary",
"script": "res://weapons/dmr.gd"
},
"plasma_gun": {
"name": "Plasma Gun",
"category": "primary",
"script": "res://weapons/plasma_gun.gd"
},
"nail_gun": {
"name": "Nail Gun",
"category": "primary",
"script": "res://weapons/nail_gun.gd"
}
}
+11
View File
@@ -3,6 +3,7 @@ extends CanvasLayer
var bg: ColorRect
var main_vbox: VBoxContainer
var resume_btn: Button
var respawn_btn: Button
var loadouts_btn: Button
var settings_btn: Button
var exit_btn: Button
@@ -66,6 +67,10 @@ func _resume() -> void:
get_tree().paused = false
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _respawn() -> void:
_resume() # Unpause
get_tree().reload_current_scene()
func _build_ui() -> void:
bg = ColorRect.new()
bg.color = Color(0, 0, 0, 0.7)
@@ -92,6 +97,11 @@ func _build_ui() -> void:
resume_btn.custom_minimum_size = Vector2(200, 50)
main_vbox.add_child(resume_btn)
respawn_btn = Button.new()
respawn_btn.text = "Respawn"
respawn_btn.custom_minimum_size = Vector2(200, 50)
main_vbox.add_child(respawn_btn)
loadouts_btn = Button.new()
loadouts_btn.text = "Loadouts"
loadouts_btn.custom_minimum_size = Vector2(200, 50)
@@ -250,6 +260,7 @@ func _create_label(text: String) -> Label:
func _connect_signals() -> void:
resume_btn.pressed.connect(_resume)
respawn_btn.pressed.connect(_respawn)
loadouts_btn.pressed.connect(_show_loadouts)
settings_btn.pressed.connect(_show_settings)
exit_btn.pressed.connect(_show_exit_dialog)
+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