Files
Papay-Shooter/weapons/homing_projectile.gd
T
Nicholas ButzkeandClaude Fable 5 a51595e3b9 feat: material-aware sound occlusion + early reflections for localization
New Acoustics system (globals/acoustics.gd):
- Occlusion: rays from listener to source collect up to 3 occluding bodies;
  each surface's material adds transmission loss and lowers the lowpass
  cutoff. A gunshot through wood stays warm (-8dB, 1.4kHz), brick muffles
  hard (-15dB, 600Hz), concrete harder (-18dB, 450Hz), metal keeps more
  highs (-11dB, 1kHz), glass barely muffles (-5dB, 2.6kHz). Applied via
  each AudioStreamPlayer3D's attenuation filter.
- Early reflections: loud sounds (gunfire, explosions) probe 8 directions
  for nearby walls; each close surface plays a quiet, delayed (path/343ms),
  material-filtered copy from the mirror point — shots audibly slap back
  off structures so players can triangulate. Sub-30ms echoes are skipped
  (they perceptually merge with the direct sound).
- Surface classification: builder-set acoustic_material meta -> node-name
  keywords -> metallic-material inference -> concrete default; cached.

Integration:
- AudioManager.play_3d applies occlusion at fire time and re-filters all
  playing positional sounds every 0.12s (walking behind a wall mid-sound
  audibly muffles it); flight loops opt in via "acoustic_occluded" group
- Remote players' gunshots were entirely SILENT — rpc_play_fire_effects now
  plays the right weapon's shot at the shooter's position, occluded and
  reflected like everything else
- Remote players' footsteps were also silent — now positional + occluded,
  cadence scaled to their speed
- Test level tagged: brick arena walls, wooden platforms/obstacles, metal
  wall-run corridor/rails/dash platforms; dust2 sandstone reads as brick

Tests: debug/acoustics_test.gd (12 checks: per-material loss ordering,
multi-wall stacking, reflection delay/filter, name classification) — all
pass; movement 11/11; smoke 0 failures.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-17 23:48:38 -04:00

137 lines
4.2 KiB
GDScript

extends ExplosiveProjectile
class_name HomingProjectile
var weapon_source: Node
var turn_speed: float = 5.0
var initial_aim_dir: Vector3 = Vector3.ZERO
var cruise_speed: float = 60.0
var ignition_timer: float = 0.5
var ignited: bool = false
var effects_started: bool = false
func _setup_effects() -> void:
pass # Wait until ignited
func _start_homing_effects() -> void:
if effects_started: return
effects_started = true
# Audio
flight_sound = AudioStreamPlayer3D.new()
flight_sound.bus = "SFX"
flight_sound.stream = load("res://assets/sounds/swarm_flight.wav")
flight_sound.autoplay = true
if flight_sound.stream is AudioStreamWAV:
flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
flight_sound.stream.loop_end = flight_sound.stream.data.size() / 2 # mono 16-bit frames
flight_sound.unit_size = 6.0
flight_sound.max_db = 0.0
flight_sound.max_distance = 45.0
flight_sound.volume_db = -10.0
flight_sound.add_to_group("acoustic_occluded") # muffles behind walls
add_child(flight_sound)
flight_sound.play()
# Jet Fire Particles
jet_sys = GPUParticles3D.new()
var jet_mat = ParticleProcessMaterial.new()
jet_mat.direction = Vector3(0, 0, 1)
jet_mat.spread = 10.0
jet_mat.initial_velocity_min = 0.5
jet_mat.initial_velocity_max = 1.0
jet_mat.gravity = Vector3.ZERO
var jet_mesh = BoxMesh.new()
jet_mesh.size = Vector3(0.02, 0.02, 0.02)
var mat = StandardMaterial3D.new()
mat.albedo_color = Color(1.0, 0.6, 0.0)
mat.emission_enabled = true
mat.emission = Color(1.0, 0.5, 0.0)
jet_mesh.material = mat
jet_sys.draw_pass_1 = jet_mesh
jet_sys.process_material = jet_mat
jet_sys.amount = 16
jet_sys.lifetime = 0.15
jet_sys.position = Vector3(0, 0, 0.05)
add_child(jet_sys)
# Smoke Trail
smoke_sys = GPUParticles3D.new()
var smoke_mat = ParticleProcessMaterial.new()
smoke_mat.direction = Vector3(0, 0, 1)
smoke_mat.spread = 5.0
smoke_mat.initial_velocity_min = 0.0
smoke_mat.initial_velocity_max = 0.2
smoke_mat.gravity = Vector3(0, 0.5, 0)
var grad = Gradient.new()
grad.add_point(0.0, Color(0.5, 0.5, 0.5, 0.8))
grad.add_point(1.0, Color(0.8, 0.8, 0.8, 0.0))
var grad_tex = GradientTexture1D.new()
grad_tex.gradient = grad
smoke_mat.color_ramp = grad_tex
var curve = Curve.new()
curve.add_point(Vector2(0.0, 1.0))
curve.add_point(Vector2(1.0, 3.0))
var curve_tex = CurveTexture.new()
curve_tex.curve = curve
smoke_mat.scale_curve = curve_tex
var smoke_mesh = QuadMesh.new()
smoke_mesh.size = Vector2(0.08, 0.08)
var s_mat = StandardMaterial3D.new()
s_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
s_mat.vertex_color_use_as_albedo = true
s_mat.albedo_texture = ExplosiveProjectile.get_smoke_texture()
s_mat.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES
s_mat.billboard_keep_scale = true
smoke_mesh.material = s_mat
smoke_sys.draw_pass_1 = smoke_mesh
smoke_sys.process_material = smoke_mat
smoke_sys.amount = 32
smoke_sys.lifetime = 1.0
smoke_sys.position = Vector3(0, 0, 0.05)
add_child(smoke_sys)
func _physics_process(delta: float) -> void:
if not ignited:
ignition_timer -= delta
if ignition_timer <= 0.0:
ignited = true
drop_gravity = 0.0
_start_homing_effects()
if ignited and is_instance_valid(weapon_source):
var target_dir: Vector3 = initial_aim_dir
var is_painting = false
var target: Vector3 = Vector3.ZERO
if weapon_source.get("player") and weapon_source.player:
is_painting = weapon_source.player.synced_is_targeting
target = weapon_source.player.synced_homing_target_pos
else:
is_painting = weapon_source.get("is_targeting")
if is_painting != null and is_painting:
target = weapon_source.get("homing_target_pos")
if is_painting and target != Vector3.ZERO:
target_dir = (target - global_position).normalized()
if target_dir != Vector3.ZERO:
var current_dir = velocity.normalized()
var new_dir = current_dir.lerp(target_dir, turn_speed * delta).normalized()
# Accelerate to cruise speed
var current_speed = velocity.length()
var new_speed = move_toward(current_speed, cruise_speed, 80.0 * delta)
velocity = new_dir * new_speed
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)