Files
Papay-Shooter/weapons/nail_gun.gd
Nicholas ButzkeandClaude Fable 5 df0bf18e0f feat: real per-weapon reload/recoil choreography + knife slash, fully matte hair
Weapons no longer reload by spinning the gun 360 (that was the upside-down
flip; nothing detached). New ViewmodelAnim module drives per-style manual-of-
arms sequences on the existing model_root + named arm pivots, with a temporary
magazine/shell/rocket/cell prop that visibly leaves and returns to the gun:

  - mag     (M4/AK/MP7/DMR): roll gun to present the well, hand rips the mag
            down and away, fresh mag up, seat, charge. Gun stays UPRIGHT.
  - boltmag (AWP): mag swap + a distinct bolt-cycle rock.
  - break   (double barrel): hinge open, flick both shells out, drop fresh
            shells in, snap shut.
  - tube    (rocket launcher): tip the tube in, shove a rocket home, shoulder.
  - cell    (plasma/nail gun): glowing energy cell swapped on the side.

Per-shot viewmodel recoil kick (ViewmodelAnim.apply_kick) scaled by each
weapon's recoil_amplitude, so an AWP slams and an MP7 chatters — replaces the
old flat model behaviour. Weapons expose reload_style; base classes call
play_reload / stop (on unequip) / apply_kick.

Knife: the old barely-visible flick is now a real diagonal slash that
alternates backhand/forehand, whips the blade through screen centre, and
fires the third-person melee arm-swing action (synced to other players).

Hair/materials: specular fully to 0 (even a 2% stepped glint swept as shine
across Taila's hair when the camera moved); rim trimmed to a whisper. Fully
matte cloth/hair now.

Verify: debug/fp_weapon_capture.gd screenshots each weapon's reload phases,
the knife slash, and the fire kick.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-21 00:26:29 -04:00

150 lines
5.2 KiB
GDScript

extends BaseProjectileWeapon
class_name NailGun
func _init() -> void:
weapon_name = "Nail Gun"
reload_style = "cell"
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 = 90.0 # Extremely fast, less lead and drop
projectile_gravity = 5.0 # Slight drop
func _build_model() -> void:
position = Vector3(0.3, -0.3, -0.604)
var model_scene = load("res://assets/weapons/nailgun/source/Sopra.fbx")
if model_scene:
var model = model_scene.instantiate()
model_root.add_child(model)
model.scale = Vector3(0.06, 0.06, 0.06)
model.rotation_degrees.y = 180
for light in model.find_children("*", "Light3D", true, false):
light.queue_free()
var mat = StandardMaterial3D.new()
mat.albedo_texture = load("res://assets/weapons/nailgun/textures/Base_texture.png")
mat.normal_enabled = true
mat.normal_texture = load("res://assets/weapons/nailgun/textures/Normal_texture.png")
mat.metallic = 1.0
mat.metallic_texture = load("res://assets/weapons/nailgun/textures/Metallic_texture.png")
mat.roughness_texture = load("res://assets/weapons/nailgun/textures/Roughness_texture.png")
mat.ao_enabled = true
mat.ao_texture = load("res://assets/weapons/nailgun/textures/internal_ground_ao_texture.jpeg")
for child in model.find_children("*", "MeshInstance3D", true, false):
child.set_surface_override_material(0, mat)
# 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.bus = "SFX"
fire_sound.stream = AudioManager.stream_for("nailgun_fire", "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.bus = "SFX"
reload_sound.stream = AudioManager.stream_for("shotgun_reload", "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)
# Tracer streak
var tracer = MeshInstance3D.new()
var t_mesh = CylinderMesh.new()
t_mesh.top_radius = 0.02
t_mesh.bottom_radius = 0.0
t_mesh.height = 1.5
var t_mat = StandardMaterial3D.new()
t_mat.albedo_color = Color(1.0, 0.8, 0.1) # Bright yellow/orange
t_mat.emission_enabled = true
t_mat.emission = Color(1.0, 0.8, 0.1)
t_mat.emission_energy_multiplier = 3.0
t_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
t_mat.albedo_color.a = 0.6
t_mesh.material = t_mat
tracer.mesh = t_mesh
tracer.rotation.x = deg_to_rad(-90)
tracer.position.z = 0.75 # Trail behind the nail
proj.add_child(tracer)
var spawn_pos = origin
var actual_fire_dir = fire_dir
if muzzle_flash and camera:
spawn_pos = muzzle_flash.global_position
# Map the position through the viewmodel camera to fix FOV mismatch
var wman = get_parent()
if wman and "vm_camera" in wman and is_instance_valid(wman.vm_camera):
if wman.vm_camera.is_position_behind(muzzle_flash.global_position) == false:
var screen_pos = wman.vm_camera.unproject_position(muzzle_flash.global_position)
var dist = camera.global_position.distance_to(muzzle_flash.global_position)
spawn_pos = camera.project_position(screen_pos, dist)
var target_pos = origin + fire_dir * max_range
var space_state = camera.get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(origin, target_pos)
if player:
query.exclude = [player.get_rid()]
var result = space_state.intersect_ray(query)
if result:
target_pos = result.position
actual_fire_dir = (target_pos - spawn_pos).normalized()
# Projectile rotation so it faces velocity
var up_vec = Vector3.UP
if abs(actual_fire_dir.dot(up_vec)) > 0.99:
up_vec = Vector3.RIGHT
proj.transform.basis = Basis.looking_at(actual_fire_dir, up_vec)
# Push the projectile forward slightly so the 1.5m long tracer tail
# doesn't clip backward past the gun and look like it's coming from beside the player's head.
proj.position = spawn_pos + actual_fire_dir * 1.5
proj.logical_source = origin
proj.velocity = actual_fire_dir * projectile_speed
if player:
proj.velocity += actual_fire_dir * max(0.0, player.velocity.dot(actual_fire_dir))
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)