spaceship/tests/test_shot_volley_loudness.gd

134 lines
4.7 KiB
GDScript3
Raw Normal View History

#229, #234: one voice per volley, and gas within reach of the spawn #229 - a volley got louder for every ship in it. Every hull owns its gun and asks for its own shot sound, so eight ships firing on the same cooldown started eight voices playing the SAME sample in the same frame. Those amplitudes add, so a full fleet was the loudest thing in the game by a wide margin. play() now takes a per-sound coalescing window. A volley is one event to the ear, so it gets one voice. 70ms, which the test checks is inside the fastest gun's 500ms cooldown - it collapses "six guns at once", never "six shots in a row". PICKUP and ASTEROID_BREAK get one too: ore arrives in bursts when a rock breaks and that is one pickup to the ear. Deaths and the boss scream are left alone deliberately - several things dying together IS several events. test_sfx's pool-saturation case was driven by shots, which can no longer saturate anything by design. It is driven by deaths now, which is what the pool actually has to absorb, and it gained a case asserting a volley takes one voice - measured against the real pool rather than the gate. #234 - gas was not reliably in reach of the opening. Gas giants are gated behind two independent noise fields, the SPACE biome AND the planet roll, so whether any sat near the spawn was luck. Measured, this seed lost: ZERO gas planets within four chunks, the nearest five away past five rocky ones. Gas is an early need - the burner, ammunition - so the opening cannot depend on the noise being kind. SPAWN_GAS_GIANT_CHUNK at (2, -2), authored like OCTOPUS_BIOME_CENTER rather than found by searching outward: a fixed chunk is reproducible from anywhere without walking the map, which is what the rest of WorldGen is built on. The override is applied AFTER every rng draw, for the same reason the dev planting is - forcing one chunk must not shift what any other chunk rolls. Confirmed by probe: the five other planets within four chunks are identical before and after, and the distant gas giants at (-5,-4) and (0,-5) are still where they were. Both verified in reverse: without the window, "8 ships firing together started 8 shot sounds"; without the chunk, "no gas planet within 4 chunks of the spawn". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
2026-09-14 10:49:25 -04:00
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