feat: add plasma gun weapon implementation and new pause menu system with loadout management
This commit is contained in:
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bddm0nooqfj2
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://l06bdsfbd0up
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://cbkr1yr1xtfum
|
||||
+112
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dy2xsq04e6ryt
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bctti6s7em04f
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://unto0q3j6jix
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bkl7mdau5363
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://n156sg54wvpp
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://davfge7rifxls
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user