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]>
150 lines
5.2 KiB
GDScript
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)
|