Feat/2 weapons foundation #6

Merged
Dotts merged 30 commits from feat/2-weapons-foundation into main 2026-06-04 22:22:16 -07:00
36 changed files with 700 additions and 4 deletions
Showing only changes of commit e6a2199777 - Show all commits
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.
+49
View File
@@ -384,4 +384,53 @@ for i in range(int(sample_rate * nail_duration)):
nail_samples.append(hit + ring + chunk)
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!")
+20
View File
@@ -42,6 +42,26 @@ var weapon_db = {
"name": "Nail Gun",
"category": "primary",
"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"
}
}
+11
View File
@@ -139,6 +139,17 @@ func spawn_damage_number(amount: float, hit_pos: Vector3) -> void:
_hit_marker_tween = create_tween()
_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:
if is_instance_valid(_machine):
return _machine
+97
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://xxh6d1k6k2nf
+19 -4
View File
@@ -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
+50
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://dcmr6nvfk267e
+70
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://3pfuatif484y
+25
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://d12r4xife61uv
+82
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://b652882c1vye7
+103
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://kw1ikqtc34wr
+167
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://d2dla2at16k6d