Audio: mood music, ambience, sound effects and creature voices (Kenney CC0 + synthesised); protocol 13
This commit is contained in:
@@ -179,7 +179,8 @@ static func _pickup(w: Node3D, WN: Node, key: String) -> Node3D:
|
||||
|
||||
static func report(w: Node3D, WN: Node, cmd: Dictionary, corr: int) -> Dictionary:
|
||||
var out := {"nodes": {}, "pieces": {}, "taken": {}, "inv": w.player.inventory.to_data(),
|
||||
"received": WN.received, "corrections": corr, "dropped": []}
|
||||
"received": WN.received, "corrections": corr, "dropped": [],
|
||||
"sounds": w.get_tree().root.get_node("Sfx").heard.map(func(h): return h[0])}
|
||||
for uid in cmd.get("uids", []):
|
||||
var n: Node3D = w.props.resource_nodes.get(uid)
|
||||
out.nodes[uid] = {"hp": n.hp, "depleted": n.depleted} if n != null else null
|
||||
|
||||
@@ -9,7 +9,7 @@ if [ ! -f .godot/global_script_class_cache.cfg ]; then
|
||||
echo "Importing the project first (fresh copy)..."
|
||||
godot --headless --path . --import >/dev/null 2>&1
|
||||
fi
|
||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_host test_tutorial test_tutorial_full test_hints test_updates test_terrain_edges test_boundary; do
|
||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_terrain_edges test_boundary; do
|
||||
extra=""
|
||||
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
|
||||
# A suite that waits on a world that failed to load would hang forever,
|
||||
|
||||
280
tests/test_audio.gd
Normal file
280
tests/test_audio.gd
Normal file
@@ -0,0 +1,280 @@
|
||||
extends SceneTree
|
||||
## Sound and music (scripts/core/sfx.gd, scripts/core/music.gd): every sound
|
||||
## and track loads; doing things makes the right sounds (chop, a tree
|
||||
## falling, mining, crafting, placing, eating, getting hurt, fishing,
|
||||
## footsteps on grass and on a wooden floor); the music follows the mood
|
||||
## (day, night, storm, the red moon, wolves after you, the Old Stag), holds on
|
||||
## a moment after danger; and the volumes are kept. Headless, so nothing is
|
||||
## heard: Sfx logs what was asked for.
|
||||
## Run: godot --headless --path . --script res://tests/test_audio.gd
|
||||
|
||||
var _fails := 0
|
||||
var _finished := false
|
||||
var A: Node
|
||||
var S: Node
|
||||
var SX: Node
|
||||
var M: Node
|
||||
var world: Node3D
|
||||
var player: CharacterBody3D
|
||||
|
||||
func check(cond: bool, what: String) -> void:
|
||||
print((" ok " if cond else " FAIL ") + what)
|
||||
if not cond:
|
||||
_fails += 1
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
func _run() -> void:
|
||||
A = root.get_node("Actions")
|
||||
S = root.get_node("Saves")
|
||||
SX = root.get_node("Sfx")
|
||||
M = root.get_node("Music")
|
||||
SX.prefs_path = "user://test_audio.json"
|
||||
DirAccess.remove_absolute(SX.prefs_path)
|
||||
S.root_dir = "user://test_worlds"
|
||||
for w in S.list_worlds():
|
||||
S.delete_world(w.id)
|
||||
await _body()
|
||||
check(_finished, "test ran to the end (no script errors)")
|
||||
for w in S.list_worlds():
|
||||
S.delete_world(w.id)
|
||||
DirAccess.remove_absolute(S.root_dir)
|
||||
DirAccess.remove_absolute(SX.prefs_path)
|
||||
SX.load_prefs()
|
||||
print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails])
|
||||
quit(1 if _fails > 0 else 0)
|
||||
|
||||
func _heard(id: String, since: int) -> int:
|
||||
var n := 0
|
||||
for i in range(since, SX.heard.size()):
|
||||
if SX.heard[i][0] == id:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
func _node(kind: String) -> Node3D:
|
||||
var best: Node3D = null
|
||||
var bd := INF
|
||||
for n in world.props.resource_nodes.values():
|
||||
if is_instance_valid(n) and n.kind == kind and not n.depleted:
|
||||
var d: float = n.global_position.distance_to(player.global_position)
|
||||
if d < bd:
|
||||
bd = d
|
||||
best = n
|
||||
return best
|
||||
|
||||
func _hold(id: String) -> int:
|
||||
for i in range(player.inventory.size()):
|
||||
var s = player.inventory.get_slot(i)
|
||||
if s != null and s.id == id:
|
||||
var tmp = player.inventory.get_slot(0)
|
||||
player.inventory.set_slot(0, s)
|
||||
player.inventory.set_slot(i, tmp)
|
||||
player.select(0)
|
||||
return 0
|
||||
return -1
|
||||
|
||||
func _body() -> void:
|
||||
print("the files")
|
||||
var bad: Array = []
|
||||
for id in SX.SOUNDS:
|
||||
for f in SX.files(id):
|
||||
var st: Variant = load(f)
|
||||
if not (st is AudioStream) or (st as AudioStream).get_length() < 0.02:
|
||||
bad.append(f)
|
||||
check(bad.is_empty(), "every sound's files load (%d sounds) %s" % [SX.SOUNDS.size(), bad])
|
||||
var lens: Array = []
|
||||
for m in ["calm", "night", "storm", "danger", "boss"]:
|
||||
var st: Variant = load(M.DIR + m + ".ogg")
|
||||
lens.append(roundi(st.get_length()) if st is AudioStream else 0)
|
||||
check(lens.all(func(l): return l >= 40), "five music tracks, 40 s or more each %s" % [lens])
|
||||
var amb := ["birds", "crickets", "fire"].filter(func(a): return load(M.AMB + a + ".ogg") is AudioStream)
|
||||
check(amb.size() == 3, "birds, crickets and the fire's crackle")
|
||||
check(AudioServer.get_bus_index("Music") > 0 and AudioServer.get_bus_index("SFX") > 0 and AudioServer.get_bus_index("Ambience") > 0,
|
||||
"Music, SFX and Ambience buses")
|
||||
|
||||
print("in the world")
|
||||
S.current_id = S.create_world("Sounds", 1337, {})
|
||||
world = load("res://scenes/world.tscn").instantiate()
|
||||
root.add_child(world)
|
||||
for _i in range(5):
|
||||
await physics_frame
|
||||
player = world.player
|
||||
player.set_physics_process(false)
|
||||
player.survival.set_process(false)
|
||||
world.day_night.frozen = true
|
||||
world.weather.set_process(false)
|
||||
world.weather.force("clear")
|
||||
world.creatures.enabled = false
|
||||
if world.wolves != null:
|
||||
world.wolves.enabled = false
|
||||
var inv: Inventory = player.inventory
|
||||
inv.add("stone_axe", 1)
|
||||
inv.add("stone_pickaxe", 1)
|
||||
|
||||
var h: int = SX.heard.size()
|
||||
var tree := _node("tree")
|
||||
_hold("stone_axe")
|
||||
player.global_position = tree.global_position + Vector3(1.2, 0, 0)
|
||||
var hits := 0
|
||||
while not tree.depleted and hits < 40:
|
||||
player.survival.stamina = 100.0
|
||||
if A.harvest_hit(player, tree, tree.global_position + Vector3(0, 1.0, 0)):
|
||||
hits += 1
|
||||
check(_heard("chop", h) == hits - 1 and _heard("tree_fall", h) == 1, "an axe on a tree: chop x%d, then it falls" % (hits - 1))
|
||||
h = SX.heard.size()
|
||||
var rock := _node("rock")
|
||||
_hold("stone_pickaxe")
|
||||
player.global_position = rock.global_position + Vector3(1.6, 0, 0)
|
||||
player.survival.stamina = 100.0
|
||||
A.harvest_hit(player, rock, rock.global_position + Vector3(0, 0.5, 0))
|
||||
check(_heard("mine", h) == 1, "a pickaxe on a boulder: mine")
|
||||
var at: Vector3 = SX.heard.back()[1]
|
||||
check(at is Vector3 and at.distance_to(rock.global_position) < 2.0, "heard where it happened")
|
||||
|
||||
h = SX.heard.size()
|
||||
var rec: Dictionary = root.get_node("ItemDB").recipes["stone_axe"]
|
||||
for id in rec.inputs:
|
||||
inv.add(id, int(rec.inputs[id]))
|
||||
check(A.craft(player, "stone_axe") and _heard("craft", h) == 1, "crafting")
|
||||
inv.add("wood_floor", 1)
|
||||
var slot := _hold("wood_floor")
|
||||
var p0 := player.global_position + Vector3(4, 0, 0)
|
||||
p0.y = world.terrain.sample_height(p0.x, p0.z)
|
||||
var r: Dictionary = world.build.placement(ItemDB_get("wood_floor"), {"position": p0, "normal": Vector3.UP,
|
||||
"collider": world.terrain.get_node("TerrainBody")}, 0.0)
|
||||
check(r.ok and A.place(player, slot, r.xf, world) and _heard("place_wood", h) == 1, "placing a floor: a plank's knock")
|
||||
|
||||
inv.add("berries", 3)
|
||||
player.survival.hunger = 40.0
|
||||
A.eat(player, _hold("berries"))
|
||||
check(_heard("eat", h) == 1, "eating")
|
||||
player.survival.damage(5.0, player.global_position + Vector3(1, 0, 0), "a test")
|
||||
check(_heard("hurt", h) == 1, "getting hurt")
|
||||
SX.heard.clear()
|
||||
|
||||
print("footsteps")
|
||||
check(player.step_sound() == "step_grass", "on the meadow: grass")
|
||||
var floor_piece: Node3D = null
|
||||
for pc in world.all_pieces():
|
||||
if pc.item_id == "wood_floor":
|
||||
floor_piece = pc
|
||||
player.set_physics_process(true)
|
||||
player.global_position = floor_piece.global_position + Vector3(0, 0.6, 0)
|
||||
player.velocity = Vector3.ZERO
|
||||
for _i in range(20):
|
||||
await physics_frame
|
||||
check(player.step_sound() == "step_wood", "on the floor you built: wood")
|
||||
player.global_position = p0 + Vector3(8, 0.3, 0)
|
||||
for _i in range(10):
|
||||
await physics_frame
|
||||
player.set_physics_process(false)
|
||||
var steps0: int = SX.count("step_grass")
|
||||
player.velocity = Vector3(5, 0, 0)
|
||||
for _i in range(60):
|
||||
player._footsteps(1.0 / 60.0, false) if player.is_on_floor() else null
|
||||
player.velocity = Vector3.ZERO
|
||||
var steps: int = SX.count("step_grass") - steps0
|
||||
check(steps >= 2 and steps <= 3, "walking 5 m: a step every stride (%d)" % steps)
|
||||
|
||||
print("fishing")
|
||||
SX.heard.clear()
|
||||
inv.add("fishing_rod", 1)
|
||||
_hold("fishing_rod")
|
||||
player.survival.stamina = 100.0
|
||||
A.cast_line(player, player.global_position + Vector3(0, 1.4, 0), Vector3(1, 0.2, 0), world)
|
||||
var bob: Node3D = world.bobber_of(player)
|
||||
check(SX.count("cast") == 1 and bob != null, "a cast")
|
||||
if bob != null:
|
||||
bob.state = "biting"
|
||||
bob.fish = "perch"
|
||||
A.reel(player, world)
|
||||
check(SX.count("reel") == 1 and SX.count("catch") == 1 and SX.count("ui_catch") == 1, "reeling a fish in: the reel, a splash, and a chime")
|
||||
|
||||
print("creature voices")
|
||||
SX.heard.clear()
|
||||
var w2: Node3D = world.creatures.spawn("wolf", player.global_position + Vector3(9, 0, 0))
|
||||
for _i in range(2):
|
||||
await physics_frame
|
||||
w2.set_physics_process(false)
|
||||
w2.state = "wander"
|
||||
w2._voice(0.1, "wander", 0.0)
|
||||
w2.state = "stalk"
|
||||
w2._voice(0.1, "wander", 0.0)
|
||||
check(SX.count("wolf_growl") == 1, "a wolf turns on you: it growls")
|
||||
for _i in range(20):
|
||||
w2._voice(0.5, "stalk", 0.0)
|
||||
check(SX.count("wolf_growl") >= 2 and SX.count("wolf_growl") <= 4, "and keeps growling now and then while it stalks (%d)" % SX.count("wolf_growl"))
|
||||
w2._attack_cd = 1.2
|
||||
w2._voice(0.1, "stalk", 0.0)
|
||||
check(SX.count("wolf_snarl") == 1, "it lunges: a snarl")
|
||||
w2.take_hit(5.0, player.global_position)
|
||||
check(SX.count("wolf_yelp") == 1, "hit it: it yelps")
|
||||
w2.take_hit(5.0, player.global_position)
|
||||
check(SX.count("wolf_yelp") == 1, "(hit twice at once: one yelp)")
|
||||
w2.queue_free()
|
||||
var missing: Array = []
|
||||
for k in w2.VOICES:
|
||||
for what in w2.VOICES[k]:
|
||||
if not SX.SOUNDS.has(String(w2.VOICES[k][what])):
|
||||
missing.append("%s.%s" % [k, what])
|
||||
check(missing.is_empty(), "every creature's voice is a sound that exists %s" % [missing])
|
||||
|
||||
print("the music")
|
||||
M.start(world)
|
||||
check(M.decide() == "calm", "a clear morning: calm")
|
||||
world.day_night.time = 23.0 / 24.0
|
||||
world.day_night.apply(true)
|
||||
check(M.decide() == "night", "after dark: night")
|
||||
world.day_night.day = 7
|
||||
check(M.decide() == "storm", "the red wolf moon: the storm music")
|
||||
world.day_night.day = 5
|
||||
world.weather.force("storm")
|
||||
world.day_night.time = 13.0 / 24.0
|
||||
world.day_night.apply(true)
|
||||
check(M.decide() == "storm", "a storm by day: storm")
|
||||
world.weather.force("clear")
|
||||
var wolf: Node3D = world.creatures.spawn("wolf", player.global_position + Vector3(12, 0, 0))
|
||||
for _i in range(3):
|
||||
await physics_frame
|
||||
wolf.set_physics_process(false)
|
||||
wolf.state = "wander"
|
||||
check(M.decide() == "calm", "a wolf wandering nearby: still calm")
|
||||
wolf.state = "stalk"
|
||||
M.step(0.6)
|
||||
check(M.mood == "danger", "the wolf comes for you: danger")
|
||||
wolf.queue_free()
|
||||
await process_frame
|
||||
for _i in range(10):
|
||||
M.step(0.5)
|
||||
check(M.mood == "danger", "it's gone, but the music holds on a moment")
|
||||
for _i in range(20):
|
||||
M.step(0.5)
|
||||
check(M.mood == "calm", "then calm again")
|
||||
var stag: Node3D = world.creatures.spawn("rabbit", player.global_position + Vector3(30, 0, 0))
|
||||
for _i in range(2):
|
||||
await physics_frame
|
||||
stag.set_physics_process(false)
|
||||
stag.kind = "old_stag"
|
||||
M.step(0.6)
|
||||
check(M.mood == "boss", "the Old Stag is up: the boss music")
|
||||
stag.queue_free()
|
||||
M.stop(world)
|
||||
M.step(0.6)
|
||||
check(M.mood == "calm", "back at the title: calm")
|
||||
|
||||
print("volumes")
|
||||
SX.set_volume("Music", 0.25)
|
||||
SX.set_volume("SFX", 0.0)
|
||||
var mb := AudioServer.get_bus_index("Music")
|
||||
check(absf(AudioServer.get_bus_volume_db(mb) - linear_to_db(0.25)) < 0.01, "Music at a quarter")
|
||||
check(AudioServer.is_bus_mute(AudioServer.get_bus_index("SFX")), "Sounds at zero: muted")
|
||||
SX.load_prefs()
|
||||
check(absf(float(SX.volumes.Music) - 0.25) < 0.001 and float(SX.volumes.SFX) == 0.0, "kept for next time")
|
||||
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
_finished = true
|
||||
|
||||
func ItemDB_get(id: String) -> Dictionary:
|
||||
return root.get_node("ItemDB").get_item(id)
|
||||
@@ -372,9 +372,15 @@ func _items_on_the_ground() -> void:
|
||||
var rep := await _bob({"do": "report"})
|
||||
check(rep.dropped.any(func(d): return d.item == "flint" and int(d.count) == 2), "Bob sees it")
|
||||
await _bob({"do": "walk", "to": _v(at + Vector3(1.0, 0, 0))})
|
||||
var SX: Node = root.get_node("Sfx")
|
||||
var heard0: int = SX.count("pickup")
|
||||
var took := await _bob({"do": "take", "key": key})
|
||||
check(_count(took.get("inv", []), "flint") == 2, "Bob picks it up")
|
||||
await _sleep(0.3)
|
||||
check(SX.count("pickup") > heard0, "Alice hears him pick it up (the server sends the sound to players nearby)")
|
||||
var brep := await _bob({"do": "report"})
|
||||
check(brep.get("sounds", []).has("pickup"), "and so does Bob")
|
||||
await _sleep(0.3)
|
||||
check(WN.dropped.is_empty() or not WN.dropped.has(int(key.substr(1))), "and it's gone for Alice")
|
||||
|
||||
print("first come, first served")
|
||||
|
||||
Reference in New Issue
Block a user