From 2c49e0c9db65bc8f437cd62f1534383933a154d1 Mon Sep 17 00:00:00 2001 From: agent-claude-cli Date: Tue, 21 Apr 2026 08:38:59 +0000 Subject: [PATCH] =?UTF-8?q?[agent:claude-cli]=20feat(audio):=20syst=C3=A8m?= =?UTF-8?q?e=20audio=20complet=20=E2=80=94=20swim=20loop,=20boost,=20block?= =?UTF-8?q?=20break,=20echo=20ping,=20pearl=20collect,=20mort/respawn,=20d?= =?UTF-8?q?eep=20sea=20ambient=20(placeholders=20AudioStreamGenerator=20si?= =?UTF-8?q?=20.ogg=20absent)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/Main.gd | 7 +- scripts/ambience/AudioManager.gd | 206 +++++++++++++++++++++++++-- scripts/dolphin/DolphinController.gd | 25 ++++ scripts/world/Pearl.gd | 3 + 4 files changed, 230 insertions(+), 11 deletions(-) diff --git a/scripts/Main.gd b/scripts/Main.gd index 8acdffb..aee2ee2 100644 --- a/scripts/Main.gd +++ b/scripts/Main.gd @@ -64,6 +64,7 @@ func _ready() -> void: AudioManager.play_ambient_loop("underwater_ambient") AudioManager.play_music("underwater_theme") AudioManager.play_whale_call_random() + AudioManager.play_deep_sea_ambient() # Starting inventory inventory.add_item(2, 10) @@ -226,11 +227,11 @@ func _on_block_break(hit_position: Vector3, _normal: Vector3) -> void: broken_id = world.break_block(hit_position) if broken_id > 0: inventory.add_item(broken_id, 1) - AudioManager.play_bubble_sfx(hit_position) + AudioManager.play_block_break(hit_position) _award_break_xp(broken_id, hit_position) else: _world_sync.server_break_block(hit_position) - AudioManager.play_bubble_sfx(hit_position) + AudioManager.play_block_break(hit_position) # Block break particle burst var bbp_script := load("res://scripts/dolphin/BlockBreakParticles.gd") @@ -335,7 +336,7 @@ func _spawn_consumable_popup(label: String, color: Color) -> void: func _on_echolocation(position: Vector3, _radius: float) -> void: - AudioManager.play_bubble_sfx(position) + # Echo ping is now fired directly from DolphinController; keep achievement hook var am: Node = get_node_or_null("/root/AchievementManager") if am != null: am.note_echolocation() diff --git a/scripts/ambience/AudioManager.gd b/scripts/ambience/AudioManager.gd index c4af826..3c4cf9b 100644 --- a/scripts/ambience/AudioManager.gd +++ b/scripts/ambience/AudioManager.gd @@ -3,6 +3,8 @@ extends Node var music_player: AudioStreamPlayer var ambient_loop: AudioStreamPlayer var whale_player: AudioStreamPlayer +var swim_loop_player: AudioStreamPlayer +var boost_player: AudioStreamPlayer var _whale_timer: Timer var _music_tween: Tween @@ -10,6 +12,9 @@ var _music_tween: Tween const MUSIC_PATH := "res://audio/music/" const SFX_PATH := "res://audio/sfx/" +# Generated stream cache (AudioStreamGenerator-based placeholders) +var _gen_streams: Dictionary = {} + func _ready() -> void: music_player = AudioStreamPlayer.new() music_player.bus = "Music" @@ -26,6 +31,18 @@ func _ready() -> void: whale_player.volume_db = -15.0 add_child(whale_player) + # Swim loop player (continuous while moving) + swim_loop_player = AudioStreamPlayer.new() + swim_loop_player.bus = "SFX" + swim_loop_player.volume_db = -18.0 + add_child(swim_loop_player) + + # Boost player (one-shot on boost start) + boost_player = AudioStreamPlayer.new() + boost_player.bus = "SFX" + boost_player.volume_db = -10.0 + add_child(boost_player) + _whale_timer = Timer.new() _whale_timer.one_shot = true _whale_timer.timeout.connect(_on_whale_timer) @@ -33,6 +50,18 @@ func _ready() -> void: _schedule_next_whale() +# Returns (or creates) a silent AudioStreamGenerator for placeholder sounds. +func _get_silent_stream(duration_sec: float = 0.5) -> AudioStreamGenerator: + var key := "silent_%.1f" % duration_sec + if _gen_streams.has(key): + return _gen_streams[key] + var gen := AudioStreamGenerator.new() + gen.mix_rate = 22050.0 + gen.buffer_length = duration_sec + _gen_streams[key] = gen + return gen + + func play_music(track_name: String) -> void: var path := MUSIC_PATH + track_name if not ResourceLoader.exists(path): @@ -56,6 +85,8 @@ func play_music(track_name: String) -> void: func play_ambient_loop(loop_name: String) -> void: var path := SFX_PATH + loop_name if not ResourceLoader.exists(path): + # Fall back to silent generator so the slot is occupied + ambient_loop.stream = _get_silent_stream(2.0) return var stream: AudioStream = load(path) @@ -84,9 +115,6 @@ func play_whale_call_random() -> void: func play_bubble_sfx(position: Vector3) -> void: var path := SFX_PATH + "bubbles.ogg" - if not ResourceLoader.exists(path): - return - var player := AudioStreamPlayer3D.new() player.bus = "SFX" player.volume_db = -8.0 @@ -95,16 +123,178 @@ func play_bubble_sfx(position: Vector3) -> void: add_child(player) player.global_position = position - var stream: AudioStream = load(path) - if stream == null: - player.queue_free() - return + if ResourceLoader.exists(path): + var stream: AudioStream = load(path) + if stream != null: + player.stream = stream + player.play() + player.finished.connect(player.queue_free) + return - player.stream = stream + # Placeholder: silent generator + player.stream = _get_silent_stream(0.3) player.play() player.finished.connect(player.queue_free) +# --- New: swim loop --- +func play_swim_loop() -> void: + if swim_loop_player.playing: + return + var path := SFX_PATH + "swim_loop.ogg" + if ResourceLoader.exists(path): + var s: AudioStream = load(path) + if s != null: + swim_loop_player.stream = s + swim_loop_player.play() + return + # Silent placeholder keeps the architecture wired + swim_loop_player.stream = _get_silent_stream(1.0) + swim_loop_player.play() + + +func stop_swim_loop() -> void: + if swim_loop_player.playing: + swim_loop_player.stop() + + +# --- New: boost sound --- +func play_boost() -> void: + var path := SFX_PATH + "boost.ogg" + if ResourceLoader.exists(path): + var s: AudioStream = load(path) + if s != null: + boost_player.stream = s + boost_player.play() + return + boost_player.stream = _get_silent_stream(0.4) + boost_player.play() + + +# --- New: block break --- +func play_block_break(position: Vector3) -> void: + var path := SFX_PATH + "block_break.ogg" + var player := AudioStreamPlayer3D.new() + player.bus = "SFX" + player.volume_db = -6.0 + player.max_distance = 15.0 + add_child(player) + player.global_position = position + if ResourceLoader.exists(path): + var s: AudioStream = load(path) + if s != null: + player.stream = s + player.play() + player.finished.connect(player.queue_free) + return + player.stream = _get_silent_stream(0.3) + player.play() + player.finished.connect(player.queue_free) + + +# --- New: echolocation ping --- +func play_echo_ping(position: Vector3) -> void: + var path := SFX_PATH + "echo_ping.ogg" + var player := AudioStreamPlayer3D.new() + player.bus = "SFX" + player.volume_db = -4.0 + player.max_distance = 40.0 + add_child(player) + player.global_position = position + if ResourceLoader.exists(path): + var s: AudioStream = load(path) + if s != null: + player.stream = s + player.play() + player.finished.connect(player.queue_free) + return + player.stream = _get_silent_stream(0.5) + player.play() + player.finished.connect(player.queue_free) + + +# --- New: pearl collect --- +func play_pearl_collect(position: Vector3) -> void: + var path := SFX_PATH + "pearl_collect.ogg" + var player := AudioStreamPlayer3D.new() + player.bus = "SFX" + player.volume_db = -5.0 + player.max_distance = 20.0 + add_child(player) + player.global_position = position + if ResourceLoader.exists(path): + var s: AudioStream = load(path) + if s != null: + player.stream = s + player.play() + player.finished.connect(player.queue_free) + return + player.stream = _get_silent_stream(0.4) + player.play() + player.finished.connect(player.queue_free) + + +# --- New: death / respawn --- +func play_death() -> void: + var path := SFX_PATH + "death.ogg" + if ResourceLoader.exists(path): + var s: AudioStream = load(path) + if s != null: + var p := AudioStreamPlayer.new() + p.bus = "SFX" + p.volume_db = -5.0 + add_child(p) + p.stream = s + p.play() + p.finished.connect(p.queue_free) + return + # silent fallback — architecture is wired + var p2 := AudioStreamPlayer.new() + p2.bus = "SFX" + p2.stream = _get_silent_stream(0.6) + add_child(p2) + p2.play() + p2.finished.connect(p2.queue_free) + + +func play_respawn() -> void: + var path := SFX_PATH + "respawn.ogg" + if ResourceLoader.exists(path): + var s: AudioStream = load(path) + if s != null: + var p := AudioStreamPlayer.new() + p.bus = "SFX" + p.volume_db = -6.0 + add_child(p) + p.stream = s + p.play() + p.finished.connect(p.queue_free) + return + var p2 := AudioStreamPlayer.new() + p2.bus = "SFX" + p2.stream = _get_silent_stream(0.5) + add_child(p2) + p2.play() + p2.finished.connect(p2.queue_free) + + +# --- New: ambient deep sea (low drone, played on top of underwater_ambient) --- +func play_deep_sea_ambient() -> void: + var path := SFX_PATH + "deep_sea_ambient.ogg" + var player := AudioStreamPlayer.new() + player.bus = "SFX" + player.volume_db = -20.0 + add_child(player) + if ResourceLoader.exists(path): + var s: AudioStream = load(path) + if s != null: + player.stream = s + player.play() + return + player.stream = _get_silent_stream(2.0) + player.play() + + func set_music_volume(db: float) -> void: music_player.volume_db = db diff --git a/scripts/dolphin/DolphinController.gd b/scripts/dolphin/DolphinController.gd index 84737bb..3d0dde0 100644 --- a/scripts/dolphin/DolphinController.gd +++ b/scripts/dolphin/DolphinController.gd @@ -40,6 +40,7 @@ var _is_dead: bool = false @onready var bubble_trail: Node = $BubbleEmitterPoint/BubbleTrail var _turn_input: float = 0.0 +var _was_boosting: bool = false func _ready() -> void: @@ -161,6 +162,21 @@ func _update_movement(delta: float) -> void: if bubble_trail: bubble_trail.set_intensity(speed_factor) + # Audio: swim loop + var am: Node = get_node_or_null("/root/AudioManager") + if am != null: + if current_speed > 0.5: + if am.has_method("play_swim_loop"): + am.call("play_swim_loop") + else: + if am.has_method("stop_swim_loop"): + am.call("stop_swim_loop") + # Boost start sound + if is_boosting and not _was_boosting: + if am.has_method("play_boost"): + am.call("play_boost") + _was_boosting = is_boosting + func _update_stats(delta: float) -> void: var changed: bool = false @@ -197,6 +213,9 @@ func _emit_stats() -> void: func _die() -> void: _is_dead = true player_died.emit() + var am: Node = get_node_or_null("/root/AudioManager") + if am != null and am.has_method("play_death"): + am.call("play_death") # Respawn after short delay await get_tree().create_timer(2.0).timeout _respawn() @@ -210,6 +229,9 @@ func _respawn() -> void: hunger = max_hunger _is_dead = false _emit_stats() + var am: Node = get_node_or_null("/root/AudioManager") + if am != null and am.has_method("play_respawn"): + am.call("play_respawn") func _animate_body() -> void: @@ -290,5 +312,8 @@ func _trigger_echolocation() -> void: echolocation_triggered.emit(global_position, 20.0) if is_instance_valid(_echo_pulse): _echo_pulse.trigger() + var am: Node = get_node_or_null("/root/AudioManager") + if am != null and am.has_method("play_echo_ping"): + am.call("play_echo_ping", global_position) await get_tree().create_timer(1.2).timeout is_echolocating = false diff --git a/scripts/world/Pearl.gd b/scripts/world/Pearl.gd index ae1de15..706daa3 100644 --- a/scripts/world/Pearl.gd +++ b/scripts/world/Pearl.gd @@ -58,5 +58,8 @@ func _on_body_entered(body: Node) -> void: return if body.is_in_group("player"): _collected = true + var am: Node = get_node_or_null("/root/AudioManager") + if am != null and am.has_method("play_pearl_collect"): + am.call("play_pearl_collect", global_position) collected.emit(ITEM_ID_PEARL) queue_free()