258 lines
9.7 KiB
GDScript
258 lines
9.7 KiB
GDScript
extends SceneTree
|
|
|
|
## Runtime release gate for the premium audio pass.
|
|
##
|
|
## Uses a real Forward+ match so it verifies autoload ordering, imported audio
|
|
## resources, a spawned player's movement players, and a live weapon loadout.
|
|
|
|
var _frames := 0
|
|
var _phase := "menu"
|
|
var _failures: Array[String] = []
|
|
|
|
const REQUIRED_EVENTS := [
|
|
"ak47_fire", "m4_fire", "mp7_fire", "dmr_fire", "awp_fire",
|
|
"shotgun_fire", "nailgun_fire", "plasma_fire", "rocket_fire",
|
|
"swarm_fire", "mortar_fire", "knife_swing", "shotgun_reload",
|
|
"explosion", "bullet_impact",
|
|
"footstep_concrete", "footstep_metal", "footstep_wood", "footstep_glass",
|
|
"wind", "slide", "wallrun", "dash", "jump", "double_jump", "vault", "land",
|
|
"grapple_launch", "grapple_latch", "grapple_reel",
|
|
"ui_hover", "ui_click", "ui_confirm", "ui_error", "ui_equip",
|
|
]
|
|
|
|
|
|
func _initialize() -> void:
|
|
change_scene_to_file("res://ui/main_menu/main_menu.tscn")
|
|
|
|
|
|
func _process(_delta: float) -> bool:
|
|
_frames += 1
|
|
if _phase == "menu" and _frames == 50:
|
|
_verify_menu_music()
|
|
_verify_manager()
|
|
var network = root.get_node_or_null("NetworkManager")
|
|
if network and network.has_method("start_singleplayer_match"):
|
|
network.start_singleplayer_match(GameMode.DEATHMATCH)
|
|
else:
|
|
_fail("NetworkManager cannot start the runtime audio match")
|
|
change_scene_to_file("res://scenes/maps/test_level/test_level.tscn")
|
|
_phase = "match"
|
|
_frames = 0
|
|
elif _phase == "match" and _frames == 180:
|
|
_verify_live_player()
|
|
_phase = "drain"
|
|
_frames = 0
|
|
unload_current_scene()
|
|
elif _phase == "drain" and _frames >= 90:
|
|
_finish()
|
|
return false
|
|
|
|
|
|
func _verify_menu_music() -> void:
|
|
var found := false
|
|
if current_scene:
|
|
for candidate in current_scene.find_children("*", "AudioStreamPlayer", true, false):
|
|
var player := candidate as AudioStreamPlayer
|
|
if player.bus == "Music":
|
|
found = true
|
|
_check(player.playing, "menu music is playing")
|
|
_check(player.stream is AudioStreamOggVorbis,
|
|
"menu music is an imported Ogg stream")
|
|
if player.stream is AudioStreamOggVorbis:
|
|
_check((player.stream as AudioStreamOggVorbis).loop,
|
|
"menu music loops")
|
|
break
|
|
_check(found, "main menu owns a Music-bus player")
|
|
|
|
|
|
func _verify_manager() -> void:
|
|
var am = root.get_node_or_null("AudioManager")
|
|
_check(am != null, "AudioManager autoload exists")
|
|
if am == null:
|
|
return
|
|
for event in REQUIRED_EVENTS:
|
|
_check(am.has_sound(event), "event resolves: %s" % event)
|
|
_check(am.weapon_sound_id("Double Barrel") == "shotgun_fire",
|
|
"multiplayer shotgun broadcast resolves to its fire family")
|
|
|
|
var expected_sends := {
|
|
"SFX": "Master", "Weapons": "SFX", "Explosions": "SFX",
|
|
"Impacts": "SFX", "Footsteps": "SFX", "Movement": "SFX",
|
|
"Ambience": "SFX", "UI": "SFX", "Wind": "SFX", "Music": "Master",
|
|
}
|
|
for bus_name in expected_sends:
|
|
var bus := AudioServer.get_bus_index(bus_name)
|
|
_check(bus >= 0, "bus exists: %s" % bus_name)
|
|
if bus >= 0:
|
|
_check(AudioServer.get_bus_send(bus) == expected_sends[bus_name],
|
|
"%s routes to %s" % [bus_name, expected_sends[bus_name]])
|
|
|
|
var master := AudioServer.get_bus_index("Master")
|
|
_check(_bus_has_effect(master, "AudioEffectHardLimiter"),
|
|
"Master has a hard limiter")
|
|
_check(_bus_has_effect(AudioServer.get_bus_index("Weapons"),
|
|
"AudioEffectCompressor"), "Weapons has transient compression")
|
|
_check(_bus_has_effect(AudioServer.get_bus_index("Explosions"),
|
|
"AudioEffectCompressor"), "Explosions has body compression")
|
|
|
|
var loop_stream: AudioStream = am.stream_for("wind")
|
|
_check(loop_stream is AudioStreamRandomizer, "wind resolves through variation stream")
|
|
if loop_stream is AudioStreamRandomizer:
|
|
var randomizer := loop_stream as AudioStreamRandomizer
|
|
_check(randomizer.streams_count >= 1, "wind has a source stream")
|
|
var child := randomizer.get_stream(0)
|
|
_check(child is AudioStreamWAV, "wind source is WAV")
|
|
if child is AudioStreamWAV:
|
|
var wav := child as AudioStreamWAV
|
|
_check(wav.loop_mode == AudioStreamWAV.LOOP_FORWARD,
|
|
"wind source has forward looping enabled")
|
|
_check(wav.loop_end > 0, "wind source has an explicit loop end")
|
|
|
|
var first = am.play_ui("ui_hover")
|
|
var suppressed = am.play_ui("ui_hover")
|
|
_check(first != null, "UI hover plays")
|
|
_check(suppressed == null, "UI hover chatter is rate-limited")
|
|
var click = am.play_ui("ui_click")
|
|
_check(click != null and click.bus == "UI", "UI click plays on the UI bus")
|
|
var explosion = am.play_3d("explosion", Vector3.ZERO)
|
|
_check(explosion != null and explosion.bus == "Explosions",
|
|
"explosion plays on the Explosions bus")
|
|
|
|
for material in ["concrete", "metal", "wood", "glass"]:
|
|
var surface := Node.new()
|
|
surface.set_meta("acoustic_material", material)
|
|
_check(Acoustics.classify(surface) == material,
|
|
"surface classifier preserves %s" % material)
|
|
surface.free()
|
|
|
|
|
|
func _verify_live_player() -> void:
|
|
var ambience := current_scene.get_node_or_null("AmbientBed") \
|
|
if current_scene else null
|
|
_check(ambience == null, "level has no always-on broadband ambience")
|
|
|
|
var player = null
|
|
for candidate in root.find_children("*", "CharacterBody3D", true, false):
|
|
if candidate.has_method("get_visual_model") \
|
|
and candidate.is_multiplayer_authority():
|
|
player = candidate
|
|
break
|
|
_check(player != null, "single-player match spawned the local player")
|
|
if player == null:
|
|
return
|
|
|
|
_check(player.has_method("surface_event"), "player exposes material event lookup")
|
|
var foot_event: String = player.surface_event("footstep")
|
|
_check(foot_event in [
|
|
"footstep_concrete", "footstep_metal", "footstep_wood", "footstep_glass"],
|
|
"live floor resolves to a footstep family (%s)" % foot_event)
|
|
player.play_footstep(8.0)
|
|
_check(player.footstep_player.bus == "Footsteps",
|
|
"local footsteps use the Footsteps bus")
|
|
_check(player.footstep_player.playing, "local material footstep plays")
|
|
|
|
player.update_slide_audio(14.0)
|
|
_check(player.slide_player.bus == "Movement", "slide uses the Movement bus")
|
|
_check(player.slide_player.stream is AudioStreamRandomizer,
|
|
"slide uses the mastered looping family")
|
|
player.update_wallrun_audio(14.0, Vector3.LEFT)
|
|
_check(player.wallrun_player.bus == "Movement", "wall-run uses the Movement bus")
|
|
_check(player.wind_player.bus == "Wind", "speed wind uses the independent Wind bus")
|
|
_check(player.grapple_shoot_player.bus == "Movement",
|
|
"grapple launch uses the Movement bus")
|
|
_check(player.grapple_latch_player.bus == "Movement",
|
|
"grapple latch uses the Movement bus")
|
|
_check(player.grapple_swing_player.bus == "Movement",
|
|
"grapple reel uses the Movement bus")
|
|
_check(player.jump_player.stream != player.double_jump_player.stream,
|
|
"ground jump and powered double jump use distinct masters")
|
|
var grapple_streams := [
|
|
player.grapple_shoot_player.stream,
|
|
player.grapple_latch_player.stream,
|
|
player.grapple_swing_player.stream,
|
|
]
|
|
_check(grapple_streams[0] != grapple_streams[1]
|
|
and grapple_streams[0] != grapple_streams[2]
|
|
and grapple_streams[1] != grapple_streams[2],
|
|
"grapple launch, latch and reel use three distinct masters")
|
|
_check(player.jump_player.stream not in grapple_streams
|
|
and player.double_jump_player.stream not in grapple_streams
|
|
and player.dash_player.stream not in grapple_streams,
|
|
"grapple stages do not reuse jump or jet-thrust masters")
|
|
_check(_randomizer_has_loop(player.grapple_swing_player.stream),
|
|
"grapple reel resolves to an explicitly looping cable stream")
|
|
player._on_movement_event("grapple_shoot", {})
|
|
_check(player.grapple_shoot_player.playing,
|
|
"grapple shoot event plays the cable launcher")
|
|
player._on_movement_event("grapple_latch", {})
|
|
_check(player.grapple_latch_player.playing,
|
|
"grapple latch event plays the hook impact")
|
|
_check(player._jet_vfx != null and player._jet_vfx.get_flame_count() == 4,
|
|
"live player owns exactly four jet flames")
|
|
var jet_before: float = player._jet_vfx._burst_left
|
|
player._on_movement_event("jump", {})
|
|
_check(is_equal_approx(player._jet_vfx._burst_left, jet_before),
|
|
"ordinary jump does not ignite the jets")
|
|
player._on_movement_event("double_jump", {})
|
|
_check(player._jet_vfx._burst_left > 0.0,
|
|
"double jump ignites the four-jet burst")
|
|
|
|
var managers: Array[Node] = player.find_children(
|
|
"WeaponManager", "WeaponManager", true, false)
|
|
_check(not managers.is_empty(), "live player has a WeaponManager")
|
|
if not managers.is_empty():
|
|
var manager = managers[0]
|
|
var checked_weapons := 0
|
|
for weapon in manager.weapons.values():
|
|
if weapon and "fire_sound" in weapon and weapon.fire_sound:
|
|
checked_weapons += 1
|
|
_check(weapon.fire_sound.bus == "Weapons",
|
|
"%s fire routes to Weapons" % weapon.weapon_name)
|
|
_check(weapon.fire_sound.stream is AudioStreamRandomizer,
|
|
"%s uses the mastered variation family" % weapon.weapon_name)
|
|
_check(weapon.fire_sound.max_polyphony >= 4,
|
|
"%s shots keep overlapping body/tails" % weapon.weapon_name)
|
|
_check(checked_weapons >= 3, "at least three live weapon players verified")
|
|
|
|
|
|
func _randomizer_has_loop(stream: AudioStream) -> bool:
|
|
if not stream is AudioStreamRandomizer:
|
|
return false
|
|
var randomizer := stream as AudioStreamRandomizer
|
|
if randomizer.streams_count < 1:
|
|
return false
|
|
var child := randomizer.get_stream(0)
|
|
return child is AudioStreamWAV \
|
|
and (child as AudioStreamWAV).loop_mode == AudioStreamWAV.LOOP_FORWARD
|
|
|
|
|
|
func _bus_has_effect(bus: int, effect_class: String) -> bool:
|
|
if bus < 0:
|
|
return false
|
|
for i in AudioServer.get_bus_effect_count(bus):
|
|
var effect := AudioServer.get_bus_effect(bus, i)
|
|
if effect and effect.is_class(effect_class):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _check(condition: bool, label: String) -> void:
|
|
if condition:
|
|
print(" PASS: ", label)
|
|
else:
|
|
_fail(label)
|
|
|
|
|
|
func _fail(label: String) -> void:
|
|
_failures.append(label)
|
|
printerr("AUDIO_FIDELITY FAIL: ", label)
|
|
|
|
|
|
func _finish() -> void:
|
|
if _failures.is_empty():
|
|
print("AUDIO_FIDELITY PASSED")
|
|
quit(0)
|
|
else:
|
|
printerr("AUDIO_FIDELITY failures=", _failures.size())
|
|
quit(1)
|