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