feat: implement weapon system, loadout manager, and specialized projectile behaviors with associated assets
This commit is contained in:
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -384,4 +384,53 @@ for i in range(int(sample_rate * nail_duration)):
|
|||||||
nail_samples.append(hit + ring + chunk)
|
nail_samples.append(hit + ring + chunk)
|
||||||
write_wav('assets/sounds/nailgun_fire.wav', nail_samples)
|
write_wav('assets/sounds/nailgun_fire.wav', nail_samples)
|
||||||
|
|
||||||
|
# 17. AWP (Thunderous crack, massive bass, slow echo)
|
||||||
|
awp_duration = 0.6
|
||||||
|
awp_samples = []
|
||||||
|
for i in range(int(sample_rate * awp_duration)):
|
||||||
|
t = i / sample_rate
|
||||||
|
noise = random.uniform(-1.0, 1.0)
|
||||||
|
crack = noise * math.exp(-t * 80.0) * 0.8
|
||||||
|
boom = math.sin(t * max(30.0, 80.0 - t * 150.0) * 2 * math.pi) * math.exp(-t * 10.0) * 0.9
|
||||||
|
awp_samples.append(crack + boom)
|
||||||
|
write_wav('assets/sounds/awp_fire.wav', awp_samples)
|
||||||
|
|
||||||
|
# 18. Rocket Launcher (Deep thud, whoosh)
|
||||||
|
rocket_duration = 0.4
|
||||||
|
rocket_samples = []
|
||||||
|
prev_r = 0.0
|
||||||
|
for i in range(int(sample_rate * rocket_duration)):
|
||||||
|
t = i / sample_rate
|
||||||
|
noise = random.uniform(-1.0, 1.0)
|
||||||
|
thud = math.sin(t * 50.0 * 2 * math.pi) * math.exp(-t * 20.0) * 0.7
|
||||||
|
alpha = 0.05
|
||||||
|
filtered = prev_r + alpha * (noise - prev_r)
|
||||||
|
prev_r = filtered
|
||||||
|
whoosh = filtered * math.exp(-t * 15.0) * 0.5
|
||||||
|
rocket_samples.append(thud + whoosh)
|
||||||
|
write_wav('assets/sounds/rocket_fire.wav', rocket_samples)
|
||||||
|
|
||||||
|
# 19. Swarm Launcher (High-pitched whistling zip)
|
||||||
|
swarm_duration = 0.2
|
||||||
|
swarm_samples = []
|
||||||
|
for i in range(int(sample_rate * swarm_duration)):
|
||||||
|
t = i / sample_rate
|
||||||
|
noise = random.uniform(-1.0, 1.0)
|
||||||
|
zip_freq = 600.0 + t * 2000.0
|
||||||
|
pew = math.sin(t * zip_freq * 2 * math.pi) * math.exp(-t * 25.0) * 0.4
|
||||||
|
swarm_samples.append(pew + noise * math.exp(-t * 50.0) * 0.2)
|
||||||
|
write_wav('assets/sounds/swarm_fire.wav', swarm_samples)
|
||||||
|
|
||||||
|
# 20. Mortar (Heavy hollow thunk)
|
||||||
|
mortar_duration = 0.3
|
||||||
|
mortar_samples = []
|
||||||
|
for i in range(int(sample_rate * mortar_duration)):
|
||||||
|
t = i / sample_rate
|
||||||
|
noise = random.uniform(-1.0, 1.0)
|
||||||
|
thunk_freq = 60.0
|
||||||
|
thunk = math.sin(t * thunk_freq * 2 * math.pi) * math.exp(-t * 15.0) * 0.7
|
||||||
|
ring = math.sin(t * 200.0 * 2 * math.pi) * math.exp(-t * 30.0) * 0.2
|
||||||
|
mortar_samples.append(thunk + ring)
|
||||||
|
write_wav('assets/sounds/mortar_fire.wav', mortar_samples)
|
||||||
|
|
||||||
print("Generated all sounds!")
|
print("Generated all sounds!")
|
||||||
|
|||||||
@@ -42,6 +42,26 @@ var weapon_db = {
|
|||||||
"name": "Nail Gun",
|
"name": "Nail Gun",
|
||||||
"category": "primary",
|
"category": "primary",
|
||||||
"script": "res://weapons/nail_gun.gd"
|
"script": "res://weapons/nail_gun.gd"
|
||||||
|
},
|
||||||
|
"awp": {
|
||||||
|
"name": "AWP",
|
||||||
|
"category": "special",
|
||||||
|
"script": "res://weapons/awp.gd"
|
||||||
|
},
|
||||||
|
"rocket_launcher": {
|
||||||
|
"name": "Rocket Launcher",
|
||||||
|
"category": "special",
|
||||||
|
"script": "res://weapons/rocket_launcher.gd"
|
||||||
|
},
|
||||||
|
"rocket_swarm": {
|
||||||
|
"name": "Rocket Swarm",
|
||||||
|
"category": "special",
|
||||||
|
"script": "res://weapons/rocket_swarm.gd"
|
||||||
|
},
|
||||||
|
"mortar": {
|
||||||
|
"name": "Mortar",
|
||||||
|
"category": "special",
|
||||||
|
"script": "res://weapons/mortar.gd"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,17 @@ func spawn_damage_number(amount: float, hit_pos: Vector3) -> void:
|
|||||||
_hit_marker_tween = create_tween()
|
_hit_marker_tween = create_tween()
|
||||||
_hit_marker_tween.tween_property(hit_marker, "modulate:a", 0.0, 0.4)
|
_hit_marker_tween.tween_property(hit_marker, "modulate:a", 0.0, 0.4)
|
||||||
|
|
||||||
|
func apply_impulse(force: Vector3) -> void:
|
||||||
|
velocity += force
|
||||||
|
# Force an upward launch to break ground contact
|
||||||
|
if force.y > 0.1:
|
||||||
|
# Tiny upward bump helps move_and_slide detach from the floor
|
||||||
|
global_position.y += 0.1
|
||||||
|
|
||||||
|
func take_damage(amount: float, hit_pos: Vector3, source: Node = null) -> void:
|
||||||
|
# Show damage number for self-damage
|
||||||
|
spawn_damage_number(amount, hit_pos)
|
||||||
|
|
||||||
func _ensure_machine() -> MovementStateMachine:
|
func _ensure_machine() -> MovementStateMachine:
|
||||||
if is_instance_valid(_machine):
|
if is_instance_valid(_machine):
|
||||||
return _machine
|
return _machine
|
||||||
|
|||||||
@@ -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 automatic: bool = true
|
||||||
@export var spread_angle: float = 0.01
|
@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 current_ammo: int = 30
|
||||||
var reloading: bool = false
|
var reloading: bool = false
|
||||||
var reload_timer: float = 0.0
|
var reload_timer: float = 0.0
|
||||||
@@ -125,14 +128,26 @@ func _shoot_hitscan() -> void:
|
|||||||
if player:
|
if player:
|
||||||
query.exclude = [player.get_rid()]
|
query.exclude = [player.get_rid()]
|
||||||
|
|
||||||
var result = space_state.intersect_ray(query)
|
var hits_remaining = penetration_count + 1
|
||||||
if result:
|
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 hit_dist = origin.distance_to(result.position)
|
||||||
var damage = base_damage
|
var damage = base_damage * current_damage_mult
|
||||||
|
|
||||||
if hit_dist > falloff_start:
|
if hit_dist > falloff_start:
|
||||||
var falloff_factor = clampf((hit_dist - falloff_start) / (max_range - falloff_start), 0.0, 1.0)
|
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"):
|
if result.collider.has_method("take_damage"):
|
||||||
result.collider.take_damage(damage, result.position, player)
|
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