diff --git a/Game/scripts/audio_manager.gd b/Game/scripts/audio_manager.gd index dde30860..288b5ccd 100644 --- a/Game/scripts/audio_manager.gd +++ b/Game/scripts/audio_manager.gd @@ -8,6 +8,7 @@ const MAX_INTENSITY := 1.0 var enabled := true var _engine_player: AudioStreamPlayer +var _engine_turbo := false func bind_tree_buttons(root: Node) -> void: @@ -50,9 +51,13 @@ func play_goal() -> void: func set_engine_state(thrust: float, turbo: bool) -> void: var amount := clamp_intensity(thrust) + var rising_turbo := should_play_turbo_cue(_engine_turbo, turbo, amount) if not enabled or amount <= 0.01: stop_engine() return + if rising_turbo: + _play_tone(260.0, 0.16, 0.16) + _engine_turbo = turbo if _engine_player == null or not is_instance_valid(_engine_player): _engine_player = AudioStreamPlayer.new() _engine_player.stream = _engine_stream() @@ -65,6 +70,7 @@ func set_engine_state(thrust: float, turbo: bool) -> void: func stop_engine() -> void: if _engine_player != null and is_instance_valid(_engine_player): _engine_player.stop() + _engine_turbo = false static func engine_pitch(thrust: float, turbo: bool) -> float: @@ -77,6 +83,10 @@ static func engine_volume(thrust: float, turbo: bool) -> float: return clampf(0.015 + amount * 0.045 + (0.025 if turbo and amount > 0.01 else 0.0), 0.0, 0.1) +static func should_play_turbo_cue(previous_turbo: bool, turbo: bool, thrust: float) -> bool: + return turbo and not previous_turbo and clamp_intensity(thrust) > 0.01 + + static func clamp_intensity(value: float) -> float: if not is_finite(value): return 0.0 diff --git a/Game/tests/cases/test_audio_manager.gd b/Game/tests/cases/test_audio_manager.gd index 53be9884..9d665f38 100644 --- a/Game/tests/cases/test_audio_manager.gd +++ b/Game/tests/cases/test_audio_manager.gd @@ -32,3 +32,10 @@ func test_engine_mix_is_bounded_and_turbo_is_audible() -> void: assert_true(AudioManager.engine_pitch(1.0, true) > AudioManager.engine_pitch(1.0, false), "turbo raises engine pitch") assert_true(AudioManager.engine_volume(1.0, true) > AudioManager.engine_volume(1.0, false), "turbo raises engine volume") assert_true(AudioManager.engine_volume(100.0, true) <= 0.1, "engine volume remains bounded") + + +func test_turbo_state_is_only_a_rising_edge_for_the_engine_cue() -> void: + assert_true(AudioManager.should_play_turbo_cue(false, true, 0.8), "turbo engagement emits a cue") + 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") diff --git a/TODO.md b/TODO.md index a93ebfb2..97631fd1 100644 --- a/TODO.md +++ b/TODO.md @@ -14,7 +14,7 @@ The training pipeline is built — see `TRAINING.md` (self-play PPO via the vend The largest gap between this and a AAA-feeling product is presentation, not code. Sequenced after the above for pragmatic reasons, but this is the highest impact per hour. -- [ ] **Audio — authored sound design remains open.** A dependency-free procedural `AudioManager` now provides safe UI/countdown/impact/goal hooks, an engine tone pitched/levelled from local thrust and turbo state, and is wired into kickoff, goal, ball-contact, and menu events; replace the placeholder tones with authored engine/turbo/impact/wall/goal/crowd/music assets after selecting distributable files and mixing them on real hardware. +- [ ] **Audio — authored sound design remains open.** A dependency-free procedural `AudioManager` now provides safe UI/countdown/impact/goal hooks, an engine tone pitched/levelled from local thrust and turbo state plus a rising-edge turbo cue, and is wired into kickoff, goal, ball-contact, and menu events; replace the placeholder tones with authored engine/turbo/impact/wall/goal/crowd/music assets after selecting distributable files and mixing them on real hardware. - [ ] Custom font + a real `Theme` resource for the HUD. `ThemeDB.fallback_font` at 10-13 px reads as a debug overlay. - [ ] **Video settings are implemented; profiling/visual QA remains.** `video_settings.gd` and the settings menu expose graphics presets, AA, vsync, FPS caps, resolution scaling, glow, and brightness, with preset-gated SDFGI/SSIL/SSAO/shadows. The remaining gate is measuring the preset ladder and image quality on low/mid-tier reference hardware in the live editor; no further control wiring is implied by this TODO. diff --git a/multiplayer-next.md b/multiplayer-next.md index 5543e7aa..a56c213e 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, and a local thrust/turbo-pitched engine loop 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, 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.