diff --git a/audio/Sfx.gd b/audio/Sfx.gd index 85dc951..18317b9 100644 --- a/audio/Sfx.gd +++ b/audio/Sfx.gd @@ -24,12 +24,16 @@ const VOICES: int = 12 const SHOT: String = "shot" const SHIP_DEATH: String = "ship_death" +const ENEMY_DEATH: String = "enemy_death" const BUILDING_DEATH: String = "building_death" +const DIALOG: String = "dialog" const SOUNDS: Dictionary = { SHOT: "res://audio/sfx_shot.wav", SHIP_DEATH: "res://audio/sfx_ship_death.wav", + ENEMY_DEATH: "res://audio/sfx_ship_death.wav", BUILDING_DEATH: "res://audio/sfx_building_death.wav", + DIALOG: "res://audio/sfx_dialog.wav", } # Per-sound level. A shot is fired constantly and a death happens once, so @@ -37,7 +41,9 @@ const SOUNDS: Dictionary = { const LEVELS: Dictionary = { SHOT: -26.0, SHIP_DEATH: -23.0, + ENEMY_DEATH: -23.0, BUILDING_DEATH: -23.0, + DIALOG: -19.0, } # Shots vary a little in pitch so a stream of them does not turn into one diff --git a/audio/make_sfx.py b/audio/make_sfx.py index 4b3934c..e9d0929 100644 --- a/audio/make_sfx.py +++ b/audio/make_sfx.py @@ -125,6 +125,28 @@ def shot(seconds=0.22): return [_saturate(v, 2.0) * _attack(i, 0.0015) for i, v in enumerate(mixed)] +def dialog(seconds=0.24): + """A short radio handshake when a transmission line appears. + + Two rising digital tones establish the signal; a small static tail keeps + it in the same physical world as the scan-line presentation without + turning every revealed character into a click.""" + n = int(RATE * seconds) + tone = [0.0] * n + for start, end, level, delay in ((520.0, 820.0, 0.55, 0.0), + (940.0, 1380.0, 0.32, 0.045)): + begin = int(RATE * delay) + part = _sweep(n - begin, start, end, 0.8) + for i, value in enumerate(part): + at = begin + i + tone[at] += value * level * _decay(i, len(part), 2.8) + static = [value * _decay(i, n, 2.1) * 0.16 + for i, value in enumerate(_noise(n, 17, lambda t: 0.55 - 0.42 * t))] + mixed = _mix(tone, static) + return [_saturate(value, 1.8) * _attack(i, 0.002) + for i, value in enumerate(mixed)] + + def explosion(seconds, sub_from, sub_to, roar_seed, debris_level, drive): """A blast in three layers on three clocks: the crack is over in a fifth of a second, the roar runs most of the length, and the debris @@ -163,6 +185,7 @@ def explosion(seconds, sub_from, sub_to, roar_seed, debris_level, drive): if __name__ == "__main__": _write("sfx_shot", shot()) + _write("sfx_dialog", dialog()) # A vessel coming apart: bright, fast, with real low end under it. _write("sfx_ship_death", explosion(1.45, 260.0, 38.0, 5, 0.16, 2.6)) # A structure: longer, deeper, and it keeps coming down afterwards. diff --git a/audio/sfx_dialog.wav b/audio/sfx_dialog.wav new file mode 100644 index 0000000..589b263 Binary files /dev/null and b/audio/sfx_dialog.wav differ diff --git a/audio/sfx_dialog.wav.import b/audio/sfx_dialog.wav.import new file mode 100644 index 0000000..249b440 --- /dev/null +++ b/audio/sfx_dialog.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dxg516krws85k" +path="res://.godot/imported/sfx_dialog.wav-d50ed12ab44ac60044a5fcd0eb2c25c1.sample" + +[deps] + +source_file="res://audio/sfx_dialog.wav" +dest_files=["res://.godot/imported/sfx_dialog.wav-d50ed12ab44ac60044a5fcd0eb2c25c1.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/enemies/Enemy.gd b/enemies/Enemy.gd index 86cb26a..8b2c50c 100644 --- a/enemies/Enemy.gd +++ b/enemies/Enemy.gd @@ -92,6 +92,7 @@ func take_damage(amount: int) -> void: hp -= amount if hp <= 0: EnemyExplosionVFX.spawn(get_tree().root, position) + Sfx.play(get_tree(), Sfx.ENEMY_DEATH) destroyed.emit() queue_free() diff --git a/tests/test_menu_scene_boundaries.gd b/tests/test_menu_scene_boundaries.gd index d825ec2..6dfb496 100644 --- a/tests/test_menu_scene_boundaries.gd +++ b/tests/test_menu_scene_boundaries.gd @@ -16,6 +16,8 @@ func _verify() -> void: return _fail("Main.tscn did not load") var main: Node = packed.instantiate() root.add_child(main) + if main.get_node_or_null("FadeOverlay/FadeRect") == null: + return _fail("Main lost the atmosphere fade overlay") for menu_name in MENUS: var menu: Node = main.get_node_or_null("UI/%s" % menu_name) if menu == null: @@ -26,6 +28,8 @@ func _verify() -> void: % [menu_name, expected]) if menu.get_script() == null: return _fail("%s has no owning script" % menu_name) + if menu.get_node_or_null("FadeOverlay") != null: + return _fail("%s incorrectly owns Main's atmosphere fade overlay" % menu_name) var source: String = FileAccess.get_file_as_string( "res://controllers/BuildController.gd") diff --git a/tests/test_sfx.gd b/tests/test_sfx.gd index f80487b..e69e254 100644 --- a/tests/test_sfx.gd +++ b/tests/test_sfx.gd @@ -2,8 +2,9 @@ extends SceneTree # Headless test: run with # godot --headless -s res://tests/test_sfx.gd -# Forgejo #146: a shot, a vessel dying and a structure coming down should be -# heard. +# Forgejo #146: shots and destruction should be heard. Enemy destruction and +# incoming narration are covered too, so visual events cannot silently miss +# the shared sound pool. # # The interesting failures here are not "no sound came out" - they are the # ones a play-through would not reveal for weeks. A voice pool that runs dry @@ -16,7 +17,7 @@ extends SceneTree # B. firing plays a shot, and it survives a fleet firing together - no # voice is lost, none is left stuck # C. a dying ship is heard, from a node that is freed on the same frame -# D. a destroyed structure is heard +# D. enemy and structure destruction are heard # E. levels are in proportion: a shot fires constantly and a death happens # once, so a shot must be the quieter of the two # F. sound keeps working while a dialog has the game stopped (#128) @@ -116,8 +117,12 @@ func _process(delta: float) -> bool: % [Sfx.VOICES * 3, Sfx.VOICES, busy]) # --- F. and a dialog does not silence it ----------------- + for voice in _pool().get_children(): + (voice as AudioStreamPlayer).stop() var narration: Control = scene.get_node("UI/NarrationBanner") narration.say("Everything stops but this.", "TEST") + if _sounding() == 0: + return _fail("a narration line appeared in silence") if not paused: return _fail("the dialog did not stop the game, so this proves nothing") for voice in _pool().get_children(): @@ -149,6 +154,19 @@ func _process(delta: float) -> bool: return _fail("a vessel died in silence") print("a vessel freed on the frame it dies is still heard") + # An enemy uses the same persistent pool. Its own node is freed on + # this frame too, so the call must happen before queue_free(). + for voice in _pool().get_children(): + (voice as AudioStreamPlayer).stop() + var enemy: Node2D = load("res://enemies/Enemy.tscn").instantiate() + scene.add_child(enemy) + enemy.take_damage(enemy.hp) + if not enemy.is_queued_for_deletion(): + return _fail("the enemy was not destroyed") + if _sounding() == 0: + return _fail("an enemy vessel exploded in silence") + print("an enemy vessel explosion is heard") + # --- D. a structure ------------------------------------ for voice in _pool().get_children(): (voice as AudioStreamPlayer).stop() diff --git a/ui/NarrationBanner.gd b/ui/NarrationBanner.gd index 24dfc19..6ee6354 100644 --- a/ui/NarrationBanner.gd +++ b/ui/NarrationBanner.gd @@ -238,6 +238,7 @@ func _advance() -> void: label.visible_characters = 0 speaker_label.text = speaker visible = true + Sfx.play(get_tree(), Sfx.DIALOG) _layout() queue_redraw() _sync_pause() diff --git a/ui/menus/FormationMenu.tscn b/ui/menus/FormationMenu.tscn index b8891d1..96f9ced 100644 --- a/ui/menus/FormationMenu.tscn +++ b/ui/menus/FormationMenu.tscn @@ -76,15 +76,3 @@ offset_top = 24 offset_bottom = 172 script = ExtResource("10") icon = 1 - -[node name="FadeOverlay" type="CanvasLayer" parent="."] -layer = 10 - -[node name="FadeRect" type="ColorRect" parent="FadeOverlay"] -mouse_filter = 2 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -color = Color(0, 0, 0, 0) diff --git a/world/Main.tscn b/world/Main.tscn index e859eda..63bd37d 100644 --- a/world/Main.tscn +++ b/world/Main.tscn @@ -493,3 +493,14 @@ offset_top = 300.0 offset_bottom = 428.0 script = ExtResource("36") +[node name="FadeOverlay" type="CanvasLayer" parent="."] +layer = 10 + +[node name="FadeRect" type="ColorRect" parent="FadeOverlay"] +mouse_filter = 2 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0, 0, 0, 0)