Add mixer/accessibility settings, caption/reduce-motion hooks, audio volume clamping, a 24-NPC performance smoke and shortened coat/dress stride for hem clipping. Record the explicit Godot 4.3-CI/4.7-local matrix and the 0.9 production scope. Headless 250/250, Python 94/94, 24-NPC budget smoke passes.
103 lines
4.6 KiB
GDScript
103 lines
4.6 KiB
GDScript
extends RefCounted
|
|
## Audio feedback: a looping crowd bed that follows the crowd mood, and radio
|
|
## crackle on open/close. All procedural, no audio assets, headless-safe.
|
|
|
|
const AudioFeedback := preload("res://src/game/audio_feedback.gd")
|
|
const CrowdSystem := preload("res://src/core/crowd_system.gd")
|
|
|
|
func test_footstep_and_landing_cues_count() -> bool:
|
|
var audio := AudioFeedback.new()
|
|
audio.play_footstep("concrete")
|
|
audio.play_footstep("asphalt")
|
|
audio.play_land()
|
|
audio.play_precision_tick()
|
|
var a := TestUtils.check_eq(audio.footsteps, 2, "footsteps are counted")
|
|
var b := TestUtils.check_eq(audio.landings, 1, "landings are counted")
|
|
var c := TestUtils.check_eq(audio.precision_ticks, 1, "precision ticks are counted")
|
|
audio.free()
|
|
return a and b and c
|
|
|
|
func test_bed_volume_follows_mood() -> bool:
|
|
var quiet: float = AudioFeedback._level_to_db(0.0)
|
|
var loud: float = AudioFeedback._level_to_db(1.0)
|
|
var mid: float = AudioFeedback._level_to_db(0.5)
|
|
var a := TestUtils.check(quiet < -30.0, "a calm street keeps the bed quiet")
|
|
var b := TestUtils.check(loud > quiet, "panic raises the bed")
|
|
var c := TestUtils.check(mid > quiet and mid < loud, "the rise is gradual")
|
|
return a and b and c
|
|
|
|
func test_crowd_level_is_clamped() -> bool:
|
|
var audio: Node = AudioFeedback.new()
|
|
audio.set_crowd_level(-1.0)
|
|
var a := TestUtils.check_eq(audio.crowd_level, 0.0, "negative mood clamps to 0")
|
|
audio.set_crowd_level(3.0)
|
|
var b := TestUtils.check_eq(audio.crowd_level, 1.0, "over-unity mood clamps to 1")
|
|
audio.free()
|
|
return a and b
|
|
|
|
func test_crowd_stream_loops() -> bool:
|
|
var audio: Node = AudioFeedback.new()
|
|
var stream: AudioStreamWAV = audio._build_crowd_stream()
|
|
var a := TestUtils.check(stream != null, "a crowd stream is generated")
|
|
var b := TestUtils.check_eq(stream.loop_mode, AudioStreamWAV.LOOP_FORWARD, "the bed loops")
|
|
var c := TestUtils.check(stream.data.size() > 1000, "the bed has audio data")
|
|
var d := TestUtils.check(stream.data.size() == stream.loop_end * 2, "loop covers the whole buffer")
|
|
audio.free()
|
|
return a and b and c and d
|
|
|
|
func test_crowd_bed_is_headless_safe() -> bool:
|
|
# No tree, no audio server: none of these may crash.
|
|
var audio: Node = AudioFeedback.new()
|
|
audio.start_crowd_bed()
|
|
var a := TestUtils.check(audio.bed_playing, "start marks the bed as playing")
|
|
audio.set_crowd_level(0.78)
|
|
var b := TestUtils.check(is_equal_approx(audio.crowd_level, 0.78), "level is stored")
|
|
audio.stop_crowd_bed()
|
|
var c := TestUtils.check(not audio.bed_playing, "stop clears the playing flag")
|
|
audio.free()
|
|
return a and b and c
|
|
|
|
func test_radio_crackle_methods() -> bool:
|
|
var audio: Node = AudioFeedback.new()
|
|
audio.play_radio_open()
|
|
audio.play_radio_close()
|
|
var a := TestUtils.check_eq(audio.radio_crackles, 2, "open and close each count a crackle")
|
|
var b := TestUtils.check(audio._free_player() == null, "no players exist before _ready, and that is safe")
|
|
audio.free()
|
|
return a and b
|
|
|
|
func test_volume_settings_are_stored_and_applied() -> bool:
|
|
var audio: Node = AudioFeedback.new()
|
|
audio.set_volumes(0.8, 0.5, 0.2)
|
|
var a := TestUtils.check(is_equal_approx(audio.master_volume, 0.8), "master volume stores")
|
|
var b := TestUtils.check(is_equal_approx(audio.sfx_volume, 0.5), "sfx volume stores")
|
|
var c := TestUtils.check(is_equal_approx(audio.ambience_volume, 0.2), "ambience volume stores")
|
|
audio.set_volumes(3.0, -1.0, 0.5)
|
|
var d := TestUtils.check(is_equal_approx(audio.master_volume, 1.0)
|
|
and is_equal_approx(audio.sfx_volume, 0.0), "volumes clamp")
|
|
audio.free()
|
|
return a and b and c and d
|
|
|
|
func test_game_manager_wires_audio() -> bool:
|
|
var src := FileAccess.get_file_as_string("res://src/game/game_manager.gd")
|
|
var a := TestUtils.check(src.contains("audio.start_crowd_bed()"), "the bed starts with the shift")
|
|
var b := TestUtils.check(src.contains("audio.set_crowd_level(float(payload.get("),
|
|
"crowd events drive the bed")
|
|
var c := TestUtils.check(src.contains("audio.stop_crowd_bed()"), "the bed stops at shift end")
|
|
var d := TestUtils.check(src.contains("audio.play_radio_open()"), "opening the radio crackles")
|
|
var e := TestUtils.check(src.contains("audio.play_radio_close()"), "closing the radio crackles")
|
|
return a and b and c and d and e
|
|
|
|
func test_mood_events_reach_the_bed() -> bool:
|
|
## The bed level is fed from the same payload the crowd reactions use.
|
|
var crowd := CrowdSystem.new()
|
|
var seen: Array = []
|
|
crowd.changed.connect(func(_p): pass)
|
|
crowd.apply_event("aggressive_behavior")
|
|
seen.append(crowd.mood)
|
|
crowd.apply_event("emergency_responder")
|
|
seen.append(crowd.mood)
|
|
var a := TestUtils.check(seen[1] > seen[0], "mood rises with a responder arrival")
|
|
crowd = null
|
|
return a
|