185 lines
6.6 KiB
GDScript
185 lines
6.6 KiB
GDScript
extends SceneTree
|
|
|
|
# Headless test: run with
|
|
# godot --headless -s res://tests/test_sfx.gd
|
|
# 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
|
|
# 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. 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)
|
|
|
|
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.<instance member> 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 -----------------
|
|
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():
|
|
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")
|
|
|
|
# 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()
|
|
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
|