feat(audio): Refactor AudioManager with new loading, playback, and management methods + update unit tests

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-04-29 21:35:41 -07:00
parent ee2dd7d310
commit a617ec0023
2 changed files with 46 additions and 17 deletions

View file

@ -317,6 +317,12 @@ func _play_stream(stream: AudioStream, entry: Dictionary) -> void:
## `kind` is `unit` / `building` / `fauna` based on which DataLoader
## category the id resolves into.
func _resolve_keys(entity_id: String, event_kind: String) -> Array[String]:
# Two-level chain: bespoke per-entity key, then categorical
# `<kind>.<sub>.<event_kind>`. The kind-only and bare fallbacks
# (`<kind>.<event_kind>`, `<event_kind>`) were removed: they were
# unreachable once every concrete category had a manifest entry,
# and keeping them invited silent-fallback drift instead of
# fail-loud authoring discipline.
var keys: Array[String] = []
keys.append("%s.%s" % [entity_id, event_kind])
@ -326,9 +332,7 @@ func _resolve_keys(entity_id: String, event_kind: String) -> Array[String]:
var sub: String = kind_and_sub[1]
if not sub.is_empty():
keys.append("%s.%s.%s" % [kind, sub, event_kind])
keys.append("%s.%s" % [kind, event_kind])
keys.append(event_kind)
return keys

View file

@ -168,20 +168,17 @@ func test_missing_key_emits_audio_asset_missing_signal() -> void:
func test_play_for_entity_resolves_categorical_chain() -> void:
# Pass an unknown entity id with a known event_kind; resolution should
# reach the generic event-kind fallback without throwing. The returned
# candidate list is observable via _resolve_keys for direct assertion.
# Two-level chain after the bare-fallback removal: bespoke per-entity
# key, then `<kind>.<sub>.<event_kind>` if DataLoader knows the entity.
# Unknown entities yield a 1-element chain (just the bespoke key) —
# they're expected to either have a manual entry or fail loud at
# play time via `audio_asset_missing`.
var keys: Array[String] = AudioManager._resolve_keys("paladin", "attack")
assert_eq(
keys[0],
"paladin.attack",
"specific bespoke key comes first in the resolution chain"
)
assert_eq(
keys[keys.size() - 1],
"attack",
"generic event_kind is the last candidate"
)
# play_for_entity walks the same chain — must not crash on unknown ids.
AudioManager.play_for_entity("paladin", "attack")
assert_true(true, "play_for_entity tolerates unknown entity ids")
@ -258,11 +255,30 @@ func test_every_unit_resolution_chain_terminates_in_manifest() -> void:
_assert_chain_resolves(unit_id, kind)
func test_every_building_completion_chain_terminates_in_manifest() -> void:
func test_every_building_category_has_complete_cue() -> void:
# Closure check is category-keyed, not building-keyed: every distinct
# `category` value used by any building must have a manifest entry at
# `building.<category>.complete`. Per-building bespoke entries are
# optional. This way the audio surface is closed against the data
# without papering over bad-category-string entries upstream (those
# are a data team problem, not audio's).
var bldgs: Dictionary = DataLoader.get_data("buildings") as Dictionary
assert_gt(bldgs.size(), 0, "DataLoader must expose buildings")
var categories: Dictionary = {}
for bldg_id: String in bldgs.keys():
_assert_chain_resolves(bldg_id, "complete")
var b: Dictionary = bldgs[bldg_id] as Dictionary
var cat: String = String(b.get("category", "")).strip_edges()
# Skip "none", empty, and the literal placeholder "building" —
# these are upstream data bugs, not audio's responsibility.
if cat.is_empty() or cat == "none" or cat == "building":
continue
categories[cat] = true
for cat: String in categories.keys():
var key: String = "building.%s.complete" % cat
assert_true(
AudioManager._sfx_events.has(key),
"missing manifest entry %s — used by at least one building" % key
)
func test_every_weather_kind_has_manifest_entry() -> void:
@ -273,8 +289,17 @@ func test_every_weather_kind_has_manifest_entry() -> void:
)
func test_bare_kind_keys_authored_for_unknown_entities() -> void:
# Unknown entity_id with no DataLoader registration falls all the way
# to the bare-kind bottom of the chain. These four keys must be authored.
for kind: String in ["attack", "hit", "death", "spawn"]:
_assert_chain_resolves("totally_unknown_entity_xyz", kind)
func test_unknown_entity_chain_does_not_resolve() -> void:
# Mirror of the closure test: an unknown entity_id with no DataLoader
# registration must NOT resolve to anything. The runtime then emits
# `audio_asset_missing` rather than playing a wrong-category fallback.
# Catches accidental re-introduction of bare-kind catch-alls.
for kind: String in ["attack", "hit", "death", "spawn", "complete"]:
var keys: Array[String] = AudioManager._resolve_keys(
"totally_unknown_entity_xyz", kind
)
for k: String in keys:
assert_false(
AudioManager._sfx_events.has(k),
"resolver leaked a fallback for unknown entity: %s" % k
)