134 lines
4.7 KiB
GDScript3
134 lines
4.7 KiB
GDScript3
|
|
extends SceneTree
|
||
|
|
|
||
|
|
# Forgejo #229: a volley does not get louder for every ship in the fleet.
|
||
|
|
#
|
||
|
|
# Each hull owns its gun and asks for its own shot sound, so six ships firing
|
||
|
|
# on the same cooldown started six voices playing the SAME sample in the same
|
||
|
|
# frame. Those amplitudes add, so the fleet's fire grew louder with every
|
||
|
|
# hull added and a full fleet was the loudest thing in the game.
|
||
|
|
#
|
||
|
|
# Covers:
|
||
|
|
# A. a whole fleet's simultaneous shots produce ONE voice, not one each
|
||
|
|
# B. the next volley is still heard - this collapses "six guns at once",
|
||
|
|
# never "six shots in a row"
|
||
|
|
# C. sounds with no window are untouched, so deaths still stack (several
|
||
|
|
# things dying together really is several events)
|
||
|
|
# D. the fleet size makes no difference to how many voices start
|
||
|
|
|
||
|
|
const VOLLEY: int = 8
|
||
|
|
|
||
|
|
|
||
|
|
func _initialize() -> void:
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
func _voices_playing() -> int:
|
||
|
|
var pool: Node = root.get_node_or_null("SfxPlayers")
|
||
|
|
if pool == null:
|
||
|
|
return 0
|
||
|
|
var n: int = 0
|
||
|
|
for child in pool.get_children():
|
||
|
|
if (child as AudioStreamPlayer).playing:
|
||
|
|
n += 1
|
||
|
|
return n
|
||
|
|
|
||
|
|
|
||
|
|
func _process(_delta: float) -> bool:
|
||
|
|
# The headless build has a Dummy audio driver, so `playing` is not a
|
||
|
|
# reliable readout. Count what play() actually DISPATCHES instead, by
|
||
|
|
# watching the coalescing clock it keeps.
|
||
|
|
Sfx.warm(self)
|
||
|
|
|
||
|
|
# --- A. one volley, one voice ---------------------------------------
|
||
|
|
Sfx._last_started_msec.clear()
|
||
|
|
var started: int = 0
|
||
|
|
for i in range(VOLLEY):
|
||
|
|
if _dispatches(Sfx.SHOT):
|
||
|
|
started += 1
|
||
|
|
if started != 1:
|
||
|
|
return _fail("%d ships firing together started %d shot sounds, expected 1" % [VOLLEY, started])
|
||
|
|
print("%d simultaneous shots -> 1 voice" % VOLLEY)
|
||
|
|
|
||
|
|
# --- D. and that holds however big the fleet is ----------------------
|
||
|
|
for size in [2, 4, 16, 32]:
|
||
|
|
Sfx._last_started_msec.clear()
|
||
|
|
var n: int = 0
|
||
|
|
for i in range(size):
|
||
|
|
if _dispatches(Sfx.SHOT):
|
||
|
|
n += 1
|
||
|
|
if n != 1:
|
||
|
|
return _fail("a fleet of %d started %d shot sounds, expected 1" % [size, n])
|
||
|
|
print("2, 4, 16 and 32 ships all start exactly 1")
|
||
|
|
|
||
|
|
# --- B. the next volley is still heard -------------------------------
|
||
|
|
Sfx._last_started_msec.clear()
|
||
|
|
if not _dispatches(Sfx.SHOT):
|
||
|
|
return _fail("the first shot of a volley was swallowed")
|
||
|
|
# Wind the clock past the window, as the next volley's cooldown would.
|
||
|
|
var window: int = int(Sfx.COALESCE_MSEC[Sfx.SHOT])
|
||
|
|
Sfx._last_started_msec[Sfx.SHOT] = Time.get_ticks_msec() - window - 1
|
||
|
|
if not _dispatches(Sfx.SHOT):
|
||
|
|
return _fail("the following volley was swallowed too - this silences sustained fire")
|
||
|
|
print("a later volley still sounds (window %dms)" % window)
|
||
|
|
|
||
|
|
# The window must be shorter than the gun it is collapsing, or it would
|
||
|
|
# start eating the weapon's own rhythm.
|
||
|
|
var fastest: float = _fastest_fire_cooldown()
|
||
|
|
if fastest <= 0.0:
|
||
|
|
return _fail("no vessel reports a fire cooldown, so this proves nothing")
|
||
|
|
var cooldown_ms: float = fastest * 1000.0
|
||
|
|
if float(window) >= cooldown_ms:
|
||
|
|
return _fail("the %dms window is not shorter than the fastest gun's %.0fms cooldown" \
|
||
|
|
% [window, cooldown_ms])
|
||
|
|
print("window %dms is inside the fastest cooldown of %.0fms" % [window, cooldown_ms])
|
||
|
|
|
||
|
|
# --- C. unwindowed sounds are untouched -------------------------------
|
||
|
|
Sfx._last_started_msec.clear()
|
||
|
|
var deaths: int = 0
|
||
|
|
for i in range(4):
|
||
|
|
if _dispatches(Sfx.SHIP_DEATH):
|
||
|
|
deaths += 1
|
||
|
|
if deaths != 4:
|
||
|
|
return _fail("4 deaths together produced %d sounds - deaths must still stack" % deaths)
|
||
|
|
print("4 simultaneous deaths still produce 4 sounds")
|
||
|
|
|
||
|
|
print("TEST_SHOT_VOLLEY_LOUDNESS: PASS")
|
||
|
|
quit(0)
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
# Would play() actually start a voice for this id right now? Mirrors the
|
||
|
|
# gate in play() rather than reaching into the audio server, which reports
|
||
|
|
# nothing useful under the Dummy driver.
|
||
|
|
func _dispatches(id: String) -> bool:
|
||
|
|
var window: int = int(Sfx.COALESCE_MSEC.get(id, 0))
|
||
|
|
if window <= 0:
|
||
|
|
Sfx.play(self, id)
|
||
|
|
return true
|
||
|
|
var now: int = Time.get_ticks_msec()
|
||
|
|
var last: int = int(Sfx._last_started_msec.get(id, -1000000))
|
||
|
|
Sfx.play(self, id)
|
||
|
|
return now - last >= window
|
||
|
|
|
||
|
|
|
||
|
|
func _fastest_fire_cooldown() -> float:
|
||
|
|
# The shortest gap any vessel's gun can fire at. ship_fire_cooldown()
|
||
|
|
# already folds in every upgrade, so this is the fastest the guns ever
|
||
|
|
# get, not just their starting rate.
|
||
|
|
var best: float = INF
|
||
|
|
var gs: Node = root.get_node("GameState")
|
||
|
|
for vessel_type in [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
|
||
|
|
GameState.VESSEL_GAS_BOMBER, GameState.VESSEL_CARRIER,
|
||
|
|
GameState.VESSEL_LASER, GameState.VESSEL_DISTORDER]:
|
||
|
|
var cooldown: float = gs.ship_fire_cooldown(vessel_type)
|
||
|
|
if cooldown > 0.0:
|
||
|
|
best = minf(best, cooldown)
|
||
|
|
if is_inf(best):
|
||
|
|
return -1.0
|
||
|
|
return best
|
||
|
|
|
||
|
|
|
||
|
|
func _fail(message: String) -> bool:
|
||
|
|
print("TEST_SHOT_VOLLEY_LOUDNESS: FAIL (%s)" % message)
|
||
|
|
quit(1)
|
||
|
|
return true
|