From 4353abfd979785060491583a48b6e42b76e3d0cf Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 1 Sep 2026 23:26:18 +0100 Subject: [PATCH] feat(audio): add wall contact cue --- Game/scripts/audio_manager.gd | 12 ++++++++++++ Game/scripts/ship.gd | 3 +++ Game/scripts/ship_camera.gd | 8 ++++++++ Game/tests/cases/test_audio_manager.gd | 5 +++++ multiplayer-next.md | 2 +- 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Game/scripts/audio_manager.gd b/Game/scripts/audio_manager.gd index 288b5ccd..fe0162f1 100644 --- a/Game/scripts/audio_manager.gd +++ b/Game/scripts/audio_manager.gd @@ -9,6 +9,7 @@ const MAX_INTENSITY := 1.0 var enabled := true var _engine_player: AudioStreamPlayer var _engine_turbo := false +var _last_wall_scrape_ms := -1000 func bind_tree_buttons(root: Node) -> void: @@ -44,6 +45,17 @@ func play_impact(intensity: float) -> void: _play_tone(150.0 + 180.0 * amount, 0.06 + 0.08 * amount, 0.08 + 0.18 * amount) +func play_wall_scrape(intensity: float) -> void: + var amount := clamp_intensity(intensity) + if amount <= 0.0: + return + var now_ms := Time.get_ticks_msec() + if now_ms - _last_wall_scrape_ms < 80: + return + _last_wall_scrape_ms = now_ms + _play_tone(110.0 + 90.0 * amount, 0.05 + 0.07 * amount, 0.05 + 0.10 * amount) + + func play_goal() -> void: _play_tone(523.25, 0.22, 0.22) _play_tone(783.99, 0.30, 0.18) diff --git a/Game/scripts/ship.gd b/Game/scripts/ship.gd index 94e49fb6..91124204 100644 --- a/Game/scripts/ship.gd +++ b/Game/scripts/ship.gd @@ -197,6 +197,7 @@ signal thrust_changed(thrust_percent: float) signal angular_velocity_changed(angular_speed: float) signal heading_changed(heading_degrees: float) signal ball_contact(intensity: float, world_position: Vector3) +signal wall_contact(intensity: float) # Performance optimization - track last emitted values to avoid unnecessary signals var _last_speed: float = -1.0 @@ -415,6 +416,8 @@ func is_turbo_active() -> bool: func _on_body_entered(body: Node) -> void: + if body is StaticBody3D: + wall_contact.emit(clampf(linear_velocity.length() / maxf(max_speed, 0.001), 0.0, 1.0)) if not body is Ball: return var relative_speed := (linear_velocity - (body as Ball).linear_velocity).length() diff --git a/Game/scripts/ship_camera.gd b/Game/scripts/ship_camera.gd index 9f81d810..e5cd610b 100644 --- a/Game/scripts/ship_camera.gd +++ b/Game/scripts/ship_camera.gd @@ -97,12 +97,16 @@ func _connect_target() -> void: return if not target.ball_contact.is_connected(_on_target_ball_contact): target.ball_contact.connect(_on_target_ball_contact) + if not target.wall_contact.is_connected(_on_target_wall_contact): + target.wall_contact.connect(_on_target_wall_contact) func _exit_tree() -> void: AudioManager.stop_engine() if is_instance_valid(target) and target.ball_contact.is_connected(_on_target_ball_contact): target.ball_contact.disconnect(_on_target_ball_contact) + if is_instance_valid(target) and target.wall_contact.is_connected(_on_target_wall_contact): + target.wall_contact.disconnect(_on_target_wall_contact) func _input(event): @@ -243,6 +247,10 @@ func _on_target_ball_contact(intensity: float, _world_position: Vector3) -> void impact_feedback.emit(intensity) +func _on_target_wall_contact(intensity: float) -> void: + AudioManager.play_wall_scrape(intensity) + + func _apply_shake(delta: float) -> void: if _shake_strength <= 0.001: _shake_strength = 0.0 diff --git a/Game/tests/cases/test_audio_manager.gd b/Game/tests/cases/test_audio_manager.gd index 9d665f38..1498e374 100644 --- a/Game/tests/cases/test_audio_manager.gd +++ b/Game/tests/cases/test_audio_manager.gd @@ -39,3 +39,8 @@ func test_turbo_state_is_only_a_rising_edge_for_the_engine_cue() -> void: assert_true(not AudioManager.should_play_turbo_cue(true, true, 0.8), "held turbo does not retrigger") assert_true(not AudioManager.should_play_turbo_cue(false, true, 0.0), "turbo at idle thrust is silent") assert_true(not AudioManager.should_play_turbo_cue(false, false, 0.8), "ordinary thrust emits no turbo cue") + + +func test_wall_scrape_intensity_reuses_the_same_safe_bounds() -> void: + assert_eq(AudioManager.clamp_intensity(-2.0), 0.0, "reverse wall intensity is silent") + assert_eq(AudioManager.clamp_intensity(2.0), 1.0, "wall intensity is capped") diff --git a/multiplayer-next.md b/multiplayer-next.md index a56c213e..cecbf593 100644 --- a/multiplayer-next.md +++ b/multiplayer-next.md @@ -1627,6 +1627,6 @@ Signed MatchNet claims now also require exact JSON string/integer types for ever Presentation progress: a shared `Game/themes/cosmic_clash_theme.tres` now gives the menu, lobby, matchmaking, and settings surfaces consistent button, input, option, and label styling. The custom-font portion of `TODO.md` remains open until a distributable font asset is selected. -The audio TODO now has a runtime foundation: `AudioManager` generates bounded placeholder tones for kickoff countdowns, camera-reported ball impacts, goals, UI clicks, a local thrust/turbo-pitched engine loop, and a rising-edge turbo cue without adding binary assets; authored sound design and production audio QA remain open. +The audio TODO now has a runtime foundation: `AudioManager` generates bounded placeholder tones for kickoff countdowns, camera-reported ball impacts, static-wall contacts, goals, UI clicks, a local thrust/turbo-pitched engine loop, and a rising-edge turbo cue without adding binary assets; authored sound design and production audio QA remain open. The video-settings TODO is likewise locally implemented: presets, vsync, refresh-derived FPS caps, and resolution scaling are wired through `VideoSettings` and the settings menu. The remaining acceptance work is low/mid-tier hardware frame-time and image-quality profiling, which cannot be certified from this workspace.