feat: implement weapon system, loadout manager, and specialized projectile behaviors with associated assets
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
extends BaseHitscanWeapon
|
||||
class_name AWP
|
||||
|
||||
var default_fov: float = 75.0
|
||||
var target_fov: float = 75.0
|
||||
var ads_fov: float = 20.0 # Extremely high zoom
|
||||
var is_ads: bool = false
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "AWP"
|
||||
fire_rate = 1.5 # Slow
|
||||
max_ammo = 3
|
||||
reload_time = 3.0
|
||||
base_damage = 150.0 # 1 hit kill
|
||||
min_damage = 130.0 # Long falloff still kills
|
||||
falloff_start = 100.0
|
||||
max_range = 300.0
|
||||
automatic = false
|
||||
spread_angle = 0.0
|
||||
penetration_count = 1
|
||||
penetration_damage_penalty = 0.5
|
||||
|
||||
func _build_model() -> void:
|
||||
position = Vector3(0.3, -0.3, -0.8)
|
||||
|
||||
var mat_green = StandardMaterial3D.new()
|
||||
mat_green.albedo_color = Color(0.1, 0.4, 0.1)
|
||||
var mat_black = StandardMaterial3D.new()
|
||||
mat_black.albedo_color = Color(0.1, 0.1, 0.1)
|
||||
|
||||
# Body
|
||||
var body = MeshInstance3D.new()
|
||||
var b_mesh = BoxMesh.new()
|
||||
b_mesh.size = Vector3(0.06, 0.12, 0.6)
|
||||
b_mesh.material = mat_green
|
||||
body.mesh = b_mesh
|
||||
model_root.add_child(body)
|
||||
|
||||
# Scope
|
||||
var scope = MeshInstance3D.new()
|
||||
var s_mesh = CylinderMesh.new()
|
||||
s_mesh.top_radius = 0.03
|
||||
s_mesh.bottom_radius = 0.03
|
||||
s_mesh.height = 0.25
|
||||
s_mesh.material = mat_black
|
||||
scope.mesh = s_mesh
|
||||
scope.rotation.x = deg_to_rad(90)
|
||||
scope.position = Vector3(0, 0.09, -0.1)
|
||||
model_root.add_child(scope)
|
||||
|
||||
# Long Barrel
|
||||
var barrel = MeshInstance3D.new()
|
||||
var bar_mesh = CylinderMesh.new()
|
||||
bar_mesh.top_radius = 0.015
|
||||
bar_mesh.bottom_radius = 0.02
|
||||
bar_mesh.height = 0.6
|
||||
bar_mesh.material = mat_black
|
||||
barrel.mesh = bar_mesh
|
||||
barrel.rotation.x = deg_to_rad(90)
|
||||
barrel.position = Vector3(0, 0.04, -0.6)
|
||||
model_root.add_child(barrel)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/awp_fire.wav")
|
||||
fire_sound.volume_db = 0.0
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # Reusing for now
|
||||
model_root.add_child(reload_sound)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
super._process(delta)
|
||||
|
||||
if camera:
|
||||
if Input.is_action_just_pressed("fire_alt"):
|
||||
is_ads = not is_ads
|
||||
target_fov = ads_fov if is_ads else default_fov
|
||||
|
||||
camera.fov = lerpf(camera.fov, target_fov, 15.0 * delta)
|
||||
|
||||
# Move model slightly based on ADS
|
||||
var target_pos = Vector3(0.0, -0.15, -0.6) if is_ads else Vector3(0.3, -0.3, -0.8)
|
||||
position = position.lerp(target_pos, 15.0 * delta)
|
||||
|
||||
func _start_reload() -> void:
|
||||
super._start_reload()
|
||||
is_ads = false # Drop out of ADS on reload
|
||||
target_fov = default_fov
|
||||
|
||||
func unequip() -> void:
|
||||
is_ads = false
|
||||
if camera:
|
||||
camera.fov = default_fov
|
||||
target_fov = default_fov
|
||||
position = Vector3(0.3, -0.3, -0.8)
|
||||
@@ -0,0 +1 @@
|
||||
uid://xxh6d1k6k2nf
|
||||
@@ -12,6 +12,9 @@ class_name BaseHitscanWeapon
|
||||
@export var automatic: bool = true
|
||||
@export var spread_angle: float = 0.01
|
||||
|
||||
@export var penetration_count: int = 0
|
||||
@export var penetration_damage_penalty: float = 0.5
|
||||
|
||||
var current_ammo: int = 30
|
||||
var reloading: bool = false
|
||||
var reload_timer: float = 0.0
|
||||
@@ -125,14 +128,26 @@ func _shoot_hitscan() -> void:
|
||||
if player:
|
||||
query.exclude = [player.get_rid()]
|
||||
|
||||
var result = space_state.intersect_ray(query)
|
||||
if result:
|
||||
var hits_remaining = penetration_count + 1
|
||||
var current_damage_mult = 1.0
|
||||
|
||||
while hits_remaining > 0:
|
||||
var result = space_state.intersect_ray(query)
|
||||
if not result:
|
||||
break # Nothing else in the way
|
||||
|
||||
var hit_dist = origin.distance_to(result.position)
|
||||
var damage = base_damage
|
||||
var damage = base_damage * current_damage_mult
|
||||
|
||||
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)
|
||||
damage = lerpf(base_damage * current_damage_mult, min_damage * current_damage_mult, falloff_factor)
|
||||
|
||||
if result.collider.has_method("take_damage"):
|
||||
result.collider.take_damage(damage, result.position, player)
|
||||
|
||||
# Add this hit to exclude so next loop goes through it
|
||||
query.exclude.append(result.rid)
|
||||
|
||||
hits_remaining -= 1
|
||||
current_damage_mult *= penetration_damage_penalty
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
extends ExplosiveProjectile
|
||||
class_name BouncingProjectile
|
||||
|
||||
var fuse_time: float = -1.0 # -1 means no fuse started yet
|
||||
var impact_detonate_time: float = 2.0
|
||||
var bounciness: float = 0.4
|
||||
var flight_time: float = 0.0
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
flight_time += delta
|
||||
|
||||
if fuse_time > 0.0:
|
||||
fuse_time -= delta
|
||||
if fuse_time <= 0.0:
|
||||
_explode(global_position)
|
||||
queue_free()
|
||||
return
|
||||
|
||||
# Apply gravity drop if any
|
||||
velocity.y -= drop_gravity * delta
|
||||
|
||||
var movement = velocity * delta
|
||||
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var query = PhysicsRayQueryParameters3D.create(_previous_position, global_position + movement)
|
||||
if owner_player:
|
||||
query.exclude = [owner_player.get_rid()]
|
||||
|
||||
var result = space_state.intersect_ray(query)
|
||||
if result:
|
||||
if flight_time > impact_detonate_time:
|
||||
# Explode on impact if flying long enough
|
||||
_on_hit(result)
|
||||
else:
|
||||
# Bounce
|
||||
global_position = result.position + result.normal * 0.05
|
||||
_previous_position = global_position
|
||||
|
||||
# Reflect velocity
|
||||
velocity = velocity.bounce(result.normal) * bounciness
|
||||
|
||||
# Start the fuse if it hasn't started
|
||||
if fuse_time < 0.0:
|
||||
fuse_time = 3.0 # Explode 3 seconds after the first bounce
|
||||
|
||||
# If it hits an entity directly, still bounce, it's a heavy mortar shell!
|
||||
else:
|
||||
global_position += movement
|
||||
distance_traveled += movement.length()
|
||||
_previous_position = global_position
|
||||
@@ -0,0 +1 @@
|
||||
uid://dcmr6nvfk267e
|
||||
@@ -0,0 +1,70 @@
|
||||
extends Projectile
|
||||
class_name ExplosiveProjectile
|
||||
|
||||
var explosion_radius: float = 5.0
|
||||
var explosion_knockback: float = 20.0
|
||||
var can_self_damage: bool = false
|
||||
var falloff_curve: float = 1.0 # 1.0 is linear, 2.0 is quadratic (steep drop)
|
||||
|
||||
func _on_hit(result: Dictionary) -> void:
|
||||
_explode(result.position)
|
||||
queue_free()
|
||||
|
||||
func _explode(pos: Vector3) -> void:
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
|
||||
# We use a sphere shape query to find all physics bodies in radius
|
||||
var sphere = SphereShape3D.new()
|
||||
sphere.radius = explosion_radius
|
||||
|
||||
var params = PhysicsShapeQueryParameters3D.new()
|
||||
params.shape = sphere
|
||||
params.transform = Transform3D(Basis(), pos)
|
||||
# Find bodies and areas
|
||||
params.collide_with_bodies = true
|
||||
|
||||
var results = space_state.intersect_shape(params)
|
||||
|
||||
# We want to only damage each object once (intersect_shape can return multiple shapes of the same object)
|
||||
var processed_rids = {}
|
||||
|
||||
for res in results:
|
||||
var col = res.collider
|
||||
if not col or processed_rids.has(res.rid):
|
||||
continue
|
||||
processed_rids[res.rid] = true
|
||||
|
||||
# Calculate distance
|
||||
var target_pos = col.global_position
|
||||
# If it's a character, maybe aim at their center (offset y by 1.0)
|
||||
if col is CharacterBody3D or col.has_method("take_damage"):
|
||||
if col.has_node("CollisionShape3D"):
|
||||
target_pos = col.get_node("CollisionShape3D").global_position
|
||||
|
||||
var dist = pos.distance_to(target_pos)
|
||||
if dist > explosion_radius:
|
||||
continue
|
||||
|
||||
# Normalised distance (0 = epicenter, 1 = edge)
|
||||
var ndist = dist / explosion_radius
|
||||
var intensity = clampf(1.0 - pow(ndist, falloff_curve), 0.0, 1.0)
|
||||
|
||||
var final_damage = damage * intensity
|
||||
var final_knockback = explosion_knockback * intensity
|
||||
|
||||
# Knockback direction: from explosion pos to target
|
||||
var dir = (target_pos - pos).normalized()
|
||||
# Add a tiny bit of upward bias to knockback so it lifts them
|
||||
dir = (dir + Vector3(0, 0.5, 0)).normalized()
|
||||
|
||||
# Apply knockback if it has apply_impulse
|
||||
if col.has_method("apply_impulse"):
|
||||
col.apply_impulse(dir * final_knockback)
|
||||
|
||||
# Apply damage
|
||||
if col.has_method("take_damage"):
|
||||
if col == owner_player and not can_self_damage:
|
||||
# Skip self damage, but we still applied the knockback!
|
||||
pass
|
||||
else:
|
||||
col.take_damage(final_damage, target_pos, owner_player)
|
||||
@@ -0,0 +1 @@
|
||||
uid://3pfuatif484y
|
||||
@@ -0,0 +1,25 @@
|
||||
extends ExplosiveProjectile
|
||||
class_name HomingProjectile
|
||||
|
||||
var weapon_source: Node
|
||||
var turn_speed: float = 5.0
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if is_instance_valid(weapon_source) and weapon_source.get("homing_target_pos") != null:
|
||||
var target: Vector3 = weapon_source.homing_target_pos
|
||||
if target != Vector3.ZERO:
|
||||
# Steer towards target
|
||||
var speed = velocity.length()
|
||||
var desired_dir = (target - global_position).normalized()
|
||||
var current_dir = velocity.normalized()
|
||||
|
||||
var new_dir = current_dir.lerp(desired_dir, turn_speed * delta).normalized()
|
||||
velocity = new_dir * speed
|
||||
|
||||
# Also rotate visually to match new direction
|
||||
var up_vec = Vector3.UP
|
||||
if abs(new_dir.dot(up_vec)) > 0.99:
|
||||
up_vec = Vector3.RIGHT
|
||||
transform.basis = Basis.looking_at(new_dir, up_vec)
|
||||
|
||||
super._physics_process(delta)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d12r4xife61uv
|
||||
@@ -0,0 +1,82 @@
|
||||
extends BaseProjectileWeapon
|
||||
class_name Mortar
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "Mortar"
|
||||
fire_rate = 1.5 # Slow
|
||||
max_ammo = 1
|
||||
reload_time = 3.0 # Extremely long
|
||||
base_damage = 200.0 # Huge damage
|
||||
min_damage = 20.0
|
||||
falloff_start = 0.0
|
||||
max_range = 500.0
|
||||
automatic = false
|
||||
spread_angle = 0.0
|
||||
|
||||
projectile_speed = 25.0 # Very slow
|
||||
projectile_gravity = 15.0 # Heavy drop
|
||||
|
||||
func _build_model() -> void:
|
||||
position = Vector3(0.3, -0.4, -0.5)
|
||||
|
||||
var mat_olive = StandardMaterial3D.new()
|
||||
mat_olive.albedo_color = Color(0.2, 0.3, 0.1)
|
||||
|
||||
# Thick short tube
|
||||
var body = MeshInstance3D.new()
|
||||
var b_mesh = CylinderMesh.new()
|
||||
b_mesh.top_radius = 0.12
|
||||
b_mesh.bottom_radius = 0.12
|
||||
b_mesh.height = 0.5
|
||||
b_mesh.material = mat_olive
|
||||
body.mesh = b_mesh
|
||||
body.rotation.x = deg_to_rad(90)
|
||||
model_root.add_child(body)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/mortar_fire.wav")
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # Reusing
|
||||
model_root.add_child(reload_sound)
|
||||
|
||||
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
var proj = load("res://weapons/bouncing_projectile.gd").new()
|
||||
|
||||
# Mesh
|
||||
var mesh_instance = MeshInstance3D.new()
|
||||
var p_mesh = SphereMesh.new()
|
||||
p_mesh.radius = 0.1
|
||||
p_mesh.height = 0.2
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.1, 0.1, 0.1) # Black bomb
|
||||
p_mesh.material = mat
|
||||
mesh_instance.mesh = p_mesh
|
||||
proj.add_child(mesh_instance)
|
||||
|
||||
# Logic
|
||||
proj.position = origin + fire_dir * 0.2
|
||||
proj.logical_source = 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
|
||||
|
||||
# Mortar Bouncing logic
|
||||
proj.fuse_time = 3.0 # Explodes 3 seconds after launch if it bounces
|
||||
proj.impact_detonate_time = 2.0 # If flying > 2s, explode on impact instead
|
||||
proj.bounciness = 0.4 # Heavy, loses momentum on bounce
|
||||
|
||||
# Explosive stats
|
||||
proj.explosion_radius = 12.0 # Very large AoE
|
||||
proj.explosion_knockback = 60.0 # Massive knockback
|
||||
proj.falloff_curve = 2.0 # Steep dropoff: "Should be an instant kill in closest 50%"
|
||||
# (0-50% = dist<6. 6/12 = 0.5. 1.0 - 0.5^2 = 0.75 multiplier. 200 * 0.75 = 150 damage -> 1 shot kill)
|
||||
proj.can_self_damage = true # Can self damage
|
||||
|
||||
get_tree().current_scene.add_child(proj)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b652882c1vye7
|
||||
@@ -0,0 +1,103 @@
|
||||
extends BaseProjectileWeapon
|
||||
class_name RocketLauncher
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "Rocket Launcher"
|
||||
fire_rate = 1.0 # Moderate, but you only have 1 in the chamber anyway
|
||||
max_ammo = 1
|
||||
reload_time = 2.5 # Extremely long reload time
|
||||
base_damage = 120.0 # High damage
|
||||
min_damage = 120.0
|
||||
falloff_start = 0.0
|
||||
max_range = 300.0
|
||||
automatic = false
|
||||
spread_angle = 0.0
|
||||
|
||||
projectile_speed = 35.0 # Moderate speed
|
||||
projectile_gravity = 0.0 # Flies straight
|
||||
|
||||
func _build_model() -> void:
|
||||
position = Vector3(0.4, -0.4, -0.6)
|
||||
|
||||
var mat_grey = StandardMaterial3D.new()
|
||||
mat_grey.albedo_color = Color(0.3, 0.3, 0.3)
|
||||
|
||||
# Main Tube
|
||||
var body = MeshInstance3D.new()
|
||||
var b_mesh = CylinderMesh.new()
|
||||
b_mesh.top_radius = 0.08
|
||||
b_mesh.bottom_radius = 0.08
|
||||
b_mesh.height = 0.8
|
||||
b_mesh.material = mat_grey
|
||||
body.mesh = b_mesh
|
||||
body.rotation.x = deg_to_rad(90)
|
||||
model_root.add_child(body)
|
||||
|
||||
# Back cone
|
||||
var cone = MeshInstance3D.new()
|
||||
var c_mesh = CylinderMesh.new()
|
||||
c_mesh.top_radius = 0.08
|
||||
c_mesh.bottom_radius = 0.04
|
||||
c_mesh.height = 0.2
|
||||
c_mesh.material = mat_grey
|
||||
cone.mesh = c_mesh
|
||||
cone.rotation.x = deg_to_rad(90)
|
||||
cone.position = Vector3(0, 0, 0.5)
|
||||
model_root.add_child(cone)
|
||||
|
||||
# Muzzle Flash
|
||||
muzzle_flash = OmniLight3D.new()
|
||||
muzzle_flash.light_color = Color(1.0, 0.6, 0.2)
|
||||
muzzle_flash.light_energy = 0.0
|
||||
muzzle_flash.omni_range = 5.0
|
||||
muzzle_flash.position = Vector3(0, 0, -0.4)
|
||||
model_root.add_child(muzzle_flash)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/rocket_fire.wav")
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # Reusing
|
||||
model_root.add_child(reload_sound)
|
||||
|
||||
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
var proj = load("res://weapons/explosive_projectile.gd").new()
|
||||
|
||||
# Mesh
|
||||
var mesh_instance = MeshInstance3D.new()
|
||||
var p_mesh = CylinderMesh.new()
|
||||
p_mesh.top_radius = 0.0
|
||||
p_mesh.bottom_radius = 0.05
|
||||
p_mesh.height = 0.2
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.8, 0.2, 0.2) # Red rocket
|
||||
p_mesh.material = mat
|
||||
mesh_instance.mesh = p_mesh
|
||||
mesh_instance.rotation.x = deg_to_rad(-90)
|
||||
proj.add_child(mesh_instance)
|
||||
|
||||
# Rotation
|
||||
var up_vec = Vector3.UP
|
||||
if abs(fire_dir.dot(up_vec)) > 0.99:
|
||||
up_vec = Vector3.RIGHT
|
||||
proj.transform.basis = Basis.looking_at(fire_dir, up_vec)
|
||||
|
||||
# Logic
|
||||
proj.position = muzzle_flash.global_position if muzzle_flash else origin
|
||||
proj.logical_source = 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
|
||||
|
||||
# Explosive stats
|
||||
proj.explosion_radius = 8.0 # Moderate AoE
|
||||
proj.explosion_knockback = 45.0 # Strong knockback!
|
||||
proj.can_self_damage = false # As requested
|
||||
|
||||
get_tree().current_scene.add_child(proj)
|
||||
@@ -0,0 +1 @@
|
||||
uid://kw1ikqtc34wr
|
||||
@@ -0,0 +1,167 @@
|
||||
extends BaseProjectileWeapon
|
||||
class_name RocketSwarm
|
||||
|
||||
var is_targeting: bool = false
|
||||
var homing_target_pos: Vector3 = Vector3.ZERO
|
||||
var targeting_laser: MeshInstance3D
|
||||
|
||||
func _init() -> void:
|
||||
weapon_name = "Rocket Swarm"
|
||||
fire_rate = 1.0 # 1 click fires all 8
|
||||
max_ammo = 4 # 4 swarms
|
||||
reload_time = 3.0
|
||||
base_damage = 20.0 # Per mini rocket
|
||||
min_damage = 20.0
|
||||
falloff_start = 0.0
|
||||
max_range = 300.0
|
||||
automatic = false
|
||||
spread_angle = 0.15 # Wide spread out of the barrel
|
||||
|
||||
projectile_speed = 25.0 # Slower so they have time to home
|
||||
|
||||
func _build_model() -> void:
|
||||
position = Vector3(0.4, -0.4, -0.6)
|
||||
|
||||
var mat_box = StandardMaterial3D.new()
|
||||
mat_box.albedo_color = Color(0.2, 0.4, 0.5)
|
||||
|
||||
# Box launcher
|
||||
var body = MeshInstance3D.new()
|
||||
var b_mesh = BoxMesh.new()
|
||||
b_mesh.size = Vector3(0.2, 0.2, 0.4)
|
||||
b_mesh.material = mat_box
|
||||
body.mesh = b_mesh
|
||||
model_root.add_child(body)
|
||||
|
||||
# Laser
|
||||
targeting_laser = MeshInstance3D.new()
|
||||
var l_mesh = CylinderMesh.new()
|
||||
l_mesh.top_radius = 0.02 # Make it thicker and more visible
|
||||
l_mesh.bottom_radius = 0.02
|
||||
l_mesh.height = 300.0
|
||||
var l_mat = StandardMaterial3D.new()
|
||||
l_mat.albedo_color = Color(1.0, 0.0, 0.0, 1.0)
|
||||
l_mat.emission_enabled = true
|
||||
l_mat.emission = Color(2.0, 0.0, 0.0) # Bright red
|
||||
l_mesh.material = l_mat
|
||||
targeting_laser.mesh = l_mesh
|
||||
targeting_laser.rotation.x = deg_to_rad(90)
|
||||
targeting_laser.position = Vector3(0, 0, -150.0)
|
||||
targeting_laser.visible = false
|
||||
model_root.add_child(targeting_laser)
|
||||
|
||||
# Audio
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
fire_sound.stream = load("res://assets/sounds/swarm_fire.wav")
|
||||
model_root.add_child(fire_sound)
|
||||
|
||||
reload_sound = AudioStreamPlayer3D.new()
|
||||
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav") # Reusing
|
||||
model_root.add_child(reload_sound)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
super._process(delta)
|
||||
|
||||
if camera:
|
||||
is_targeting = Input.is_action_pressed("fire_alt")
|
||||
targeting_laser.visible = is_targeting
|
||||
|
||||
if is_targeting:
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var origin = camera.global_position
|
||||
var target = origin - camera.global_transform.basis.z * 300.0
|
||||
var query = PhysicsRayQueryParameters3D.create(origin, target)
|
||||
if player:
|
||||
query.exclude = [player.get_rid()]
|
||||
|
||||
var result = space_state.intersect_ray(query)
|
||||
if result:
|
||||
homing_target_pos = result.position
|
||||
# Shrink laser to hit point
|
||||
var dist = origin.distance_to(result.position)
|
||||
(targeting_laser.mesh as CylinderMesh).height = dist
|
||||
targeting_laser.position.z = -dist / 2.0
|
||||
else:
|
||||
homing_target_pos = target
|
||||
(targeting_laser.mesh as CylinderMesh).height = 300.0
|
||||
targeting_laser.position.z = -150.0
|
||||
|
||||
func unequip() -> void:
|
||||
is_targeting = false
|
||||
targeting_laser.visible = false
|
||||
|
||||
# Override fire to shoot 8 rockets at once
|
||||
func _fire() -> void:
|
||||
if current_ammo <= 0 and not reloading:
|
||||
_start_reload()
|
||||
return
|
||||
|
||||
if not camera: return
|
||||
|
||||
current_ammo -= 1
|
||||
fire_cooldown = fire_rate
|
||||
|
||||
if fire_sound:
|
||||
fire_sound.play()
|
||||
|
||||
if muzzle_flash:
|
||||
muzzle_flash.light_energy = 2.0
|
||||
var tween = create_tween()
|
||||
tween.tween_property(muzzle_flash, "light_energy", 0.0, 0.1)
|
||||
|
||||
var origin = camera.global_position
|
||||
var base_dir = -camera.global_transform.basis.z
|
||||
|
||||
for i in range(8):
|
||||
# Calculate spread
|
||||
var right = camera.global_transform.basis.x
|
||||
var up = camera.global_transform.basis.y
|
||||
|
||||
var x_spread = randf_range(-1.0, 1.0) * spread_angle
|
||||
var y_spread = randf_range(-1.0, 1.0) * spread_angle
|
||||
|
||||
var fire_dir = (base_dir + right * x_spread + up * y_spread).normalized()
|
||||
_spawn_custom_projectile(origin, fire_dir)
|
||||
|
||||
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
var proj = load("res://weapons/homing_projectile.gd").new()
|
||||
|
||||
# Mesh
|
||||
var mesh_instance = MeshInstance3D.new()
|
||||
var p_mesh = CylinderMesh.new()
|
||||
p_mesh.top_radius = 0.0
|
||||
p_mesh.bottom_radius = 0.02
|
||||
p_mesh.height = 0.1
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.8, 0.8, 0.1) # Yellow mini rocket
|
||||
p_mesh.material = mat
|
||||
mesh_instance.mesh = p_mesh
|
||||
mesh_instance.rotation.x = deg_to_rad(-90)
|
||||
proj.add_child(mesh_instance)
|
||||
|
||||
# Rotation
|
||||
var up_vec = Vector3.UP
|
||||
if abs(fire_dir.dot(up_vec)) > 0.99:
|
||||
up_vec = Vector3.RIGHT
|
||||
proj.transform.basis = Basis.looking_at(fire_dir, up_vec)
|
||||
|
||||
# Logic
|
||||
proj.weapon_source = self
|
||||
proj.turn_speed = 12.0 # Faster turn radius so they can aggressively home in
|
||||
|
||||
proj.position = origin # Start at origin to spread outward around camera
|
||||
proj.logical_source = 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 = 0.0
|
||||
proj.owner_player = player
|
||||
|
||||
# Explosive stats
|
||||
proj.explosion_radius = 3.0 # Tiny AoE effect
|
||||
proj.explosion_knockback = 5.0
|
||||
proj.can_self_damage = false
|
||||
|
||||
get_tree().current_scene.add_child(proj)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d2dla2at16k6d
|
||||
Reference in New Issue
Block a user