extends Node # Dependency-free audio foundation. Authored assets can replace these tones # later without changing gameplay call sites or the multiplayer event flow. const SAMPLE_RATE := 44100 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: if root == null: return for node in root.find_children("*", "BaseButton", true, false): bind_button(node as BaseButton) func bind_button(button: BaseButton) -> void: if button == null: return var callback := Callable(self, "play_ui_click") if not button.pressed.is_connected(callback): button.pressed.connect(callback) func play_ui_click() -> void: _play_tone(880.0, 0.045, 0.10) func play_countdown(count: int) -> void: if count <= 0: _play_tone(1046.5, 0.12, 0.18) else: _play_tone(countdown_frequency(count), 0.08, 0.14) func play_impact(intensity: float) -> void: var amount := clamp_intensity(intensity) if amount <= 0.0: return _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) 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() add_child(_engine_player) _engine_player.play() _engine_player.pitch_scale = engine_pitch(amount, turbo) _engine_player.volume_db = linear_to_db(engine_volume(amount, turbo)) 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: var amount := clamp_intensity(thrust) return 0.75 + amount * 0.55 + (0.30 if turbo and amount > 0.01 else 0.0) static func engine_volume(thrust: float, turbo: bool) -> float: var amount := clamp_intensity(thrust) 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 return clampf(value, 0.0, MAX_INTENSITY) static func countdown_frequency(count: int) -> float: return 440.0 + float(clampi(count, 1, 9)) * 55.0 func _play_tone(frequency: float, duration: float, volume: float) -> void: if not enabled or frequency <= 0.0 or duration <= 0.0 or volume <= 0.0: return var stream := AudioStreamWAV.new() stream.format = AudioStreamWAV.FORMAT_16_BITS stream.mix_rate = SAMPLE_RATE stream.stereo = false stream.data = _tone_data(frequency, duration, volume) var player := AudioStreamPlayer.new() player.stream = stream add_child(player) player.finished.connect(player.queue_free) player.play() func _engine_stream() -> AudioStreamWAV: var stream := AudioStreamWAV.new() stream.format = AudioStreamWAV.FORMAT_16_BITS stream.mix_rate = SAMPLE_RATE stream.stereo = false stream.loop_mode = AudioStreamWAV.LOOP_FORWARD stream.data = _tone_data(92.0, 1.0, 0.65) stream.loop_end = SAMPLE_RATE return stream func _tone_data(frequency: float, duration: float, volume: float) -> PackedByteArray: var frames := maxi(1, int(duration * SAMPLE_RATE)) var data := PackedByteArray() data.resize(frames * 2) for index in frames: var envelope := minf(1.0, float(index) / 256.0) * minf(1.0, float(frames - index) / 1024.0) var sample := int(sin(TAU * frequency * float(index) / SAMPLE_RATE) * volume * envelope * 32767.0) if sample < 0: sample += 65536 data[index * 2] = sample & 0xff data[index * 2 + 1] = (sample >> 8) & 0xff return data