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. # # 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 # under a firing fleet and silently drops every shot after the twelfth. A # sound started by a node that is being freed on the same frame, which is # every death in the game. So those are what is checked. # # Covers: # A. every sound the catalog names actually exists and loads # 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 # 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) var game_state: Node var scene: Node var fleet: Node2D var stage: int = 0 var elapsed: float = 0.0 func _initialize() -> void: # GameState is an autoload: a bare GameState. does not # compile in a SceneTree script, so the node is fetched by name instead. game_state = root.get_node("GameState") game_state.reset_new_game() game_state.intro_active = false game_state.reset_quests() game_state.rocks = 500 func _fail(message: String) -> bool: print("TEST_SFX: FAIL (%s)" % message) quit(1) return true func _pool() -> Node: return root.get_node_or_null(Sfx.NODE_NAME) func _sounding() -> int: var pool: Node = _pool() if pool == null: return 0 var n: int = 0 for voice in pool.get_children(): if (voice as AudioStreamPlayer).playing: n += 1 return n func _process(delta: float) -> bool: elapsed += delta match stage: 0: # --- A. the catalog is real ----------------------------- for id in Sfx.SOUNDS: var path: String = String(Sfx.SOUNDS[id]) var stream: AudioStream = load(path) if stream == null: return _fail("the \"%s\" sound (%s) does not load" % [id, path]) if stream.get_length() <= 0.02: return _fail("the \"%s\" sound is %.3fs long - that is silence" \ % [id, stream.get_length()]) if not Sfx.LEVELS.has(id): return _fail("the \"%s\" sound has no level set" % id) print("%d sounds, all loading, all with a level" % Sfx.SOUNDS.size()) # --- E. and in proportion ------------------------------- if float(Sfx.LEVELS[Sfx.SHOT]) >= float(Sfx.LEVELS[Sfx.SHIP_DEATH]): return _fail("a shot is as loud as a death - one happens constantly, the other once") print("shot at %.0fdB, ship death at %.0fdB, structure at %.0fdB" \ % [Sfx.LEVELS[Sfx.SHOT], Sfx.LEVELS[Sfx.SHIP_DEATH], Sfx.LEVELS[Sfx.BUILDING_DEATH]]) scene = load("res://world/Main.tscn").instantiate() root.add_child(scene) current_scene = scene fleet = scene.get_node("Fleet") elapsed = 0.0 stage = 1 return false 1: if elapsed < 0.4: return false scene.get_node("AsteroidField").set_process(false) scene.get_node("EnemySpawner").set_process(false) if _pool() != null and _sounding() > 0: return _fail("something was already sounding before anything happened") # --- B. a shot, and then far more than there are voices -- Sfx.play(self, Sfx.SHOT) if _sounding() != 1: return _fail("firing played %d sounds, expected 1" % _sounding()) if _pool().get_parent() != root: return _fail("the sound pool hangs off the scene - a death would take it with it") for i in range(Sfx.VOICES * 3): Sfx.play(self, Sfx.SHOT) var busy: int = _sounding() if busy != Sfx.VOICES: return _fail("%d voices sounding after %d shots, the pool holds %d" \ % [busy, Sfx.VOICES * 3, Sfx.VOICES]) if _pool().get_child_count() != Sfx.VOICES: return _fail("the pool grew to %d voices" % _pool().get_child_count()) print("%d shots into a %d-voice pool: %d sounding, none leaked" \ % [Sfx.VOICES * 3, Sfx.VOICES, busy]) # --- F. and a dialog does not silence it ----------------- var narration: Control = scene.get_node("UI/NarrationBanner") narration.say("Everything stops but this.", "TEST") if not paused: return _fail("the dialog did not stop the game, so this proves nothing") for voice in _pool().get_children(): if not (voice as AudioStreamPlayer).can_process(): return _fail("sound stops whenever a dialog is up") print("a dialog stops the game and sound carries on") while narration.dismiss(): pass for voice in _pool().get_children(): (voice as AudioStreamPlayer).stop() elapsed = 0.0 stage = 2 return false 2: if elapsed < 0.3: return false # --- C. a death, from a node freed on the same frame ----- fleet.spawn_ships(2, Vector2.ZERO, PackedStringArray([GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_INTERCEPTOR])) for voice in _pool().get_children(): (voice as AudioStreamPlayer).stop() var doomed: Node2D = fleet.ships[0] doomed.take_damage(1) if not is_instance_valid(doomed) or not doomed.is_queued_for_deletion(): return _fail("the ship was not freed, so this is not the case that matters") if _sounding() == 0: return _fail("a vessel died in silence") print("a vessel freed on the frame it dies is still heard") # --- D. a structure ------------------------------------ for voice in _pool().get_children(): (voice as AudioStreamPlayer).stop() var field: Node2D = scene.get_node("BuildingField") var id: int = game_state.add_building(GameState.BUILDING_SHIP_CRAFTER, Vector2(400.0, 0.0)) field.rebuild_all() game_state.destroy_building(id) if _sounding() == 0: return _fail("a structure came down in silence") print("a structure coming down is heard") print("TEST_SFX: PASS") quit(0) return true return false