feat(@projects): ✨ add audio system integration
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
89784e2331
commit
0d26c271b3
10 changed files with 223 additions and 3 deletions
155
.project/audio-status.md
Normal file
155
.project/audio-status.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# Audio status — what's done, what's left
|
||||
|
||||
**Date:** 2026-04-28
|
||||
**Objectives in flight:** `p2-33` (system extension, **done**), `p2-16` (asset acquisition, **in_progress** — code/tooling complete, asset curation 65 / 65 done, in-game smoke test pending).
|
||||
|
||||
## Tally
|
||||
|
||||
| Layer | State |
|
||||
|---|---|
|
||||
| Manifest schema (`audio.schema.json`) | ✓ ships — typed `streams[]`, `pitch_jitter`, `fallback`, `_silent` sentinel |
|
||||
| `audio_manager.gd` extensions | ✓ ships — random variant pick, fallback chain, `play_for_entity`, 12 EventBus wires |
|
||||
| Manifest entries (`audio.json`) | ✓ 49 SFX + 8 music tracks (added `defeat_stinger` + `defeat`) |
|
||||
| Validator (`audio-validate.py`) | ✓ schema, asset coherence, orphan scan |
|
||||
| License renderer (`audio-licenses-render.py`) | ✓ renders LICENSES.md, rejects -SA / -NC, allowlist gate |
|
||||
| Batch driver (`audio-fetch-batch.sh`) | ✓ idempotent, supports direct URLs + `zip#inner` paths |
|
||||
| `./run audio` family | ✓ status / fetch / fetch:NN / design / validate / render |
|
||||
| Design app `/audio` page | ✓ resolution playground + manifest browser + mixer + music list, ▶ synth + ● real buttons |
|
||||
| Vite glob hardening | ✓ `server.watch` + `server.fs.allow` so new files HMR cleanly |
|
||||
| `.gitignore` for stray `.js` outputs | ✓ design-app source tree |
|
||||
| **Real .ogg files on disk** | ✓ **65 / 65** — validator OK, no orphans, no missing |
|
||||
| GUT test `test_audio_manager.gd` | ✓ 13 / 13 pass on apricot headless |
|
||||
| Design doc + interactive page | ✓ `audio-system.md` + React app at `/audio` |
|
||||
|
||||
## Outstanding work (priority order)
|
||||
|
||||
### P0 — to close p2-16
|
||||
|
||||
1. **In-game audible smoke test on plum/apricot.**
|
||||
Boot the live game (not the design app), fire each EventBus signal kind manually:
|
||||
`turn_started` / `turn_ended` / `city_founded` / `city_grew` / `city_starved` /
|
||||
`tech_researched` / `culture_researched` / `combat_started` / `combat_resolved` /
|
||||
`unit_destroyed` / `wonder_built` / `era_changed` / `golden_age_started` /
|
||||
`victory_achieved` / `player_eliminated` (defeat) / `wild_creature_spawned` /
|
||||
`weather_event_applied`. Confirm each plays through the right bus
|
||||
(Master / Music / SFX sliders affect them as expected).
|
||||
*Why this is non-trivial*: requires a live game session with real
|
||||
players and rendering, not headless. Output: a smoke-checklist
|
||||
markdown at `.project/screenshots/audio-smoke-2026-XX-XX.md` with
|
||||
timestamps + perceived-volume notes.
|
||||
|
||||
2. **Apricot deployment** — rsync new `.ogg` files to the run host so
|
||||
the in-game session and any autoplay batches actually have audio.
|
||||
Already covered by the existing `tools/deploy.sh` rsync (assets are
|
||||
tracked); just run it.
|
||||
|
||||
3. **`p2-16` close-out**: flip status `in_progress → done` once the
|
||||
smoke test lands + screenshot attached.
|
||||
|
||||
### P1 — quality-of-life follow-ups
|
||||
|
||||
4. **Bespoke melee variants per dwarf unit.** Categorical
|
||||
`unit.melee.attack` covers everything; the `play_for_entity` ladder
|
||||
already supports `<unit_id>.attack` if you want to give Spearmen vs
|
||||
Halberdiers different swings later. One-line manifest entry per
|
||||
bespoke voicing, no code change.
|
||||
|
||||
5. **Music swap on golden age.**
|
||||
`audio_manager.gd::_on_golden_age_started` switches to the
|
||||
`golden_age` track via `play_music`. On `golden_age_ended` it calls
|
||||
`stop_music()`. Verify the era track resumes correctly on the next
|
||||
`era_changed` (or proactively re-queue it). Two-line patch to
|
||||
`_on_golden_age_ended`.
|
||||
|
||||
6. **Pre-roll defeat music as the final fade-out.** Right now
|
||||
`defeat_stinger` and `defeat` music play simultaneously on
|
||||
`player_eliminated`. Consider a 0.5 s gap so the stinger is heard
|
||||
cleanly before the music swells. Trivial in
|
||||
`_on_player_eliminated`.
|
||||
|
||||
7. **Loudness audit.** All files normalised at `loudnorm I=-16 TP=-3`
|
||||
one-pass. For music tracks, two-pass loudnorm gives more accurate
|
||||
integrated LUFS. Run `ffmpeg -af loudnorm=…:print_format=json` to
|
||||
get measurement, then re-encode. Optional polish.
|
||||
|
||||
8. **Music transition crossfade on era change.** Already implemented
|
||||
(`_crossfade_seconds = 2.0`). Verify it sounds right with the actual
|
||||
Junkala tracks — they may need 4-5 s rather than 2.
|
||||
|
||||
9. **Credits screen entry.** `LICENSES.md` is the canonical source;
|
||||
the player-facing credits screen (`scenes/menus/credits.gd`) should
|
||||
pull author lines for CC-BY entries automatically. Currently we
|
||||
have one CC-BY entry (Bogart VGM defeat music). Either render a
|
||||
`credits-audio.json` from `sources.csv` or read it directly.
|
||||
|
||||
### P2 — wider integrations
|
||||
|
||||
10. **HUD entry points** for tech tree + culture tree (deferred from
|
||||
the tree work). Currently the proof scenes open them directly; the
|
||||
live HUD has no button or hotkey. Two scene edits + EventBus
|
||||
`tech_tree_opened` / `culture_tree_opened`. About 30 LOC each.
|
||||
|
||||
11. **Per-instance pitch jitter on `unit_moved`.** The throttle
|
||||
keeps it tame, but every footstep is identical. Adding
|
||||
`pitch_jitter: 0.05` to that manifest entry would diversify it.
|
||||
One-line change.
|
||||
|
||||
12. **Mod overrides** — `user://overrides/audio.json` merged over the
|
||||
theme manifest at `AudioManager.load_theme(…)`. Hook reserved in
|
||||
p2-33 notes; ~10 LOC.
|
||||
|
||||
13. **Game 2 / Game 3 packs.** When Kzzykt and Elves ship,
|
||||
`public/games/<theme>/data/audio.json` + `assets/audio/**` follow
|
||||
the same shape. The runtime (`audio_manager.gd`) is already
|
||||
theme-agnostic; the validator + renderer accept `--theme` so
|
||||
re-running them on each pack works.
|
||||
|
||||
14. **Per-unit voice lines (post-EA exploration).** "Aye!" / "For the
|
||||
Mountain!" — out of scope for launch but the per-entity
|
||||
resolution ladder already supports
|
||||
`<unit_id>.select` and `<unit_id>.command` keys when authoring
|
||||
catches up.
|
||||
|
||||
### P3 — polish / not blocking launch
|
||||
|
||||
15. **Design app: download `.ogg` from page.** Right-click → save
|
||||
works already, but a small "download" link next to each ● button
|
||||
would let designers grab files for tweaking outside the pipeline.
|
||||
Optional.
|
||||
|
||||
16. **Per-pillar weather mood.** Weather-event SFX is currently
|
||||
one-shot. A persistent ambient layer that fades up when a storm is
|
||||
over the camera would be richer; needs a new bus or stem layering
|
||||
in `audio_manager.gd`. Out of scope for launch.
|
||||
|
||||
## Source-of-truth files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `public/games/age-of-dwarves/data/audio.json` | Canonical manifest (49 SFX + 8 music) |
|
||||
| `public/games/age-of-dwarves/assets/audio/sources.csv` | Provenance ledger — 65 rows, all licences verified |
|
||||
| `public/games/age-of-dwarves/assets/audio/LICENSES.md` | Auto-rendered from sources.csv; CI fails on hand-edit |
|
||||
| `public/games/age-of-dwarves/assets/audio/{sfx,music}/**.ogg` | The shipped pack |
|
||||
| `tools/audio-batch-{01..05}-*.tsv` | Per-pack mappings, idempotent |
|
||||
| `tools/audio-fetch-batch.sh` / `audio-validate.py` / `audio-licenses-render.py` | Pipeline tools |
|
||||
| `scripts/run/audio.sh` | `./run audio:*` family |
|
||||
| `src/game/engine/src/autoloads/audio_manager.gd` | The engine (~430 LOC) |
|
||||
| `src/game/engine/tests/unit/test_audio_manager.gd` | GUT cases (13 / 13 pass) |
|
||||
| `.project/designs/audio-system.md` | Static design doc |
|
||||
| `.project/designs/app/src/pages/AudioSystem.tsx` | Interactive design page |
|
||||
| `.project/audio-sourcing-checklist.md` | Per-row punch list (now 100% closed for the 65-file pack) |
|
||||
|
||||
## Source provenance summary
|
||||
|
||||
| Pack | Licence | Files | Source page |
|
||||
|---|---|---|---|
|
||||
| Calinou/kenney-interface-sounds (GitHub) | CC0-1.0 | 11 | https://github.com/Calinou/kenney-interface-sounds |
|
||||
| Kenney Impact Sounds | CC0-1.0 | 26 | https://kenney.nl/assets/impact-sounds |
|
||||
| Junkala JRPG Pack 2 (Towns) | CC0-1.0 | 4 | https://opengameart.org/content/jrpg-pack-2-towns |
|
||||
| Junkala JRPG Pack 5 (Action) | CC0-1.0 | 3 | https://opengameart.org/content/jrpg-pack-5-action |
|
||||
| rubberduck "30 CC0 SFX loops" | CC0-1.0 | 6 | https://opengameart.org/content/30-cc0-sfx-loops |
|
||||
| rubberduck "80 CC0 creature SFX" | CC0-1.0 | 12 | https://opengameart.org/content/80-cc0-creature-sfx |
|
||||
| Bogart VGM "Game over man" | CC-BY-4.0 (1 attr line) | 1 | https://opengameart.org/content/gb-victory-stinger-and-game-over-man |
|
||||
| Calinou kenney-interface-sounds (extras) | CC0-1.0 | 2 | https://github.com/Calinou/kenney-interface-sounds |
|
||||
|
||||
**Licence breakdown:** 64 × CC0-1.0 + 1 × CC-BY-4.0 (Bogart VGM, attribution recorded in `sources.csv` and rendered to `LICENSES.md`).
|
||||
|
|
@ -4,11 +4,30 @@ import react from "@vitejs/plugin-react";
|
|||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 7777 },
|
||||
resolve: {
|
||||
alias: {
|
||||
"@game-data": path.resolve(__dirname, "../../../public/games/age-of-dwarves/data"),
|
||||
"@game-assets": path.resolve(__dirname, "../../../public/games/age-of-dwarves/assets"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 7777,
|
||||
fs: {
|
||||
// Allow Vite to serve files from the parent project tree (assets +
|
||||
// data live outside the design app's own root).
|
||||
allow: [
|
||||
path.resolve(__dirname),
|
||||
path.resolve(__dirname, "../../../public"),
|
||||
],
|
||||
},
|
||||
watch: {
|
||||
// import.meta.glob bakes its result at module-eval time. When new
|
||||
// .ogg files appear under public/games/.../assets/audio after the
|
||||
// module first loaded, the glob doesn't see them and the design
|
||||
// page misses their play buttons. Telling Vite's watcher to track
|
||||
// the assets dir triggers an HMR reload of the AudioSystem module
|
||||
// when files are added/removed there, which re-evaluates the glob.
|
||||
ignored: ["!**/public/games/age-of-dwarves/assets/audio/**"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@
|
|||
|
||||
Each row records one `.ogg` shipped under `public/games/age-of-dwarves/assets/audio/`. Licence policy: CC0 / CC-BY 3.0 / CC-BY 4.0 / Pixabay / Sonniss-GDC-YYYY / Public-Domain accepted. ShareAlike (`-SA`) and NonCommercial (`-NC`) are rejected by the renderer.
|
||||
|
||||
**Asset count:** 68 files. (Empty until p2-16 sourcing begins.)
|
||||
**Asset count:** 70 files. (Empty until p2-16 sourcing begins.)
|
||||
|
||||
## Assets
|
||||
|
||||
| Path | License | Source | Attribution | Edits | Added |
|
||||
|------|---------|--------|-------------|-------|-------|
|
||||
| `audio/music/defeat.ogg` | CC-BY-4.0 | [link](https://opengameart.org/sites/default/files/Game%20over%20man_0.mp3) | Bogart VGM (OpenGameArt) | loudnorm I=-16/TP=-3+mp3→ogg 128kbps | 2026-04-28 |
|
||||
| `audio/music/golden_age.ogg` | CC0-1.0 | [link](https://opengameart.org/sites/default/files/JRPG%20Music%20Pack%20%235%20%5BAction%5D%20by%20Juhani%20Junkala.zip#Action2 - Army Approaching.ogg) | Juhani Junkala (SubspaceAudio | OpenGameArt) | loudnorm I=-16/TP=-3+ogg 128kbps |
|
||||
| `audio/music/overworld_ascension.ogg` | CC0-1.0 | [link](https://opengameart.org/sites/default/files/JRPG%20Music%20Pack%20%235%20%5BAction%5D%20by%20Juhani%20Junkala.zip#Action1 - Encounter With The Witches.ogg) | Juhani Junkala (SubspaceAudio | OpenGameArt) | loudnorm I=-16/TP=-3+ogg 128kbps |
|
||||
| `audio/music/overworld_awakening.ogg` | CC0-1.0 | [link](https://opengameart.org/sites/default/files/JRPG%20Music%20Pack%20%232%20%5BTowns%5D%20by%20Juhani%20Junkala.zip#Town1 - Home Town.ogg) | Juhani Junkala (SubspaceAudio | OpenGameArt) | loudnorm I=-16/TP=-3+ogg 128kbps |
|
||||
|
|
@ -28,6 +29,7 @@ Each row records one `.ogg` shipped under `public/games/age-of-dwarves/assets/au
|
|||
| `audio/sfx/combat/combat_started.ogg` | CC0-1.0 | [link](https://kenney.nl/media/pages/assets/impact-sounds/8aa7b545c9-1677589768/kenney_impact-sounds.zip#Audio/impactBell_heavy_000.ogg) | Kenney (Impact Sounds) | loudnorm I=-16/TP=-3+ogg 128kbps | 2026-04-27 |
|
||||
| `audio/sfx/combat_hit.ogg` | CC0-1.0 | [link](https://kenney.nl/media/pages/assets/impact-sounds/8aa7b545c9-1677589768/kenney_impact-sounds.zip#Audio/impactMetal_medium_000.ogg) | Kenney (Impact Sounds) | loudnorm I=-16/TP=-3+ogg 128kbps | 2026-04-27 |
|
||||
| `audio/sfx/culture_researched.ogg` | CC0-1.0 | [link](https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/confirmation_003.wav) | Kenney (Calinou repackage) | loudnorm I=-16/TP=-3+wav→ogg 128kbps | 2026-04-27 |
|
||||
| `audio/sfx/defeat_stinger.ogg` | CC0-1.0 | [link](https://kenney.nl/media/pages/assets/impact-sounds/8aa7b545c9-1677589768/kenney_impact-sounds.zip#Audio/impactPlate_heavy_004.ogg) | Kenney (Impact Sounds) | loudnorm I=-16/TP=-3+ogg 128kbps | 2026-04-28 |
|
||||
| `audio/sfx/era_advanced.ogg` | CC0-1.0 | [link](https://kenney.nl/media/pages/assets/impact-sounds/8aa7b545c9-1677589768/kenney_impact-sounds.zip#Audio/impactBell_heavy_003.ogg) | Kenney (Impact Sounds) | loudnorm I=-16/TP=-3+ogg 128kbps | 2026-04-27 |
|
||||
| `audio/sfx/fauna/apex_attack.ogg` | CC0-1.0 | [link](https://opengameart.org/sites/default/files/80-CC0-creature-SFX_0.zip#monster_01.ogg) | rubberduck (OpenGameArt) | loudnorm I=-16/TP=-3+ogg 128kbps | 2026-04-27 |
|
||||
| `audio/sfx/fauna/apex_death.ogg` | CC0-1.0 | [link](https://opengameart.org/sites/default/files/80-CC0-creature-SFX_0.zip#scream_02.ogg) | rubberduck (OpenGameArt) | loudnorm I=-16/TP=-3+ogg 128kbps | 2026-04-27 |
|
||||
|
|
|
|||
BIN
public/games/age-of-dwarves/assets/audio/music/defeat.ogg
Normal file
BIN
public/games/age-of-dwarves/assets/audio/music/defeat.ogg
Normal file
Binary file not shown.
BIN
public/games/age-of-dwarves/assets/audio/sfx/defeat_stinger.ogg
Normal file
BIN
public/games/age-of-dwarves/assets/audio/sfx/defeat_stinger.ogg
Normal file
Binary file not shown.
|
|
@ -84,3 +84,5 @@ audio/sfx/fauna/apex_attack.ogg,https://opengameart.org/sites/default/files/80-C
|
|||
audio/sfx/fauna/apex_death.ogg,https://opengameart.org/sites/default/files/80-CC0-creature-SFX_0.zip#scream_02.ogg,CC0-1.0,rubberduck (OpenGameArt),loudnorm I=-16/TP=-3+ogg 128kbps,2026-04-27
|
||||
audio/sfx/fauna/herbivore_call.ogg,https://opengameart.org/sites/default/files/80-CC0-creature-SFX_0.zip#cute_05.ogg,CC0-1.0,rubberduck (OpenGameArt),loudnorm I=-16/TP=-3+ogg 128kbps,2026-04-27
|
||||
audio/sfx/fauna/herbivore_death.ogg,https://opengameart.org/sites/default/files/80-CC0-creature-SFX_0.zip#hurt_04.ogg,CC0-1.0,rubberduck (OpenGameArt),loudnorm I=-16/TP=-3+ogg 128kbps,2026-04-27
|
||||
audio/sfx/defeat_stinger.ogg,https://kenney.nl/media/pages/assets/impact-sounds/8aa7b545c9-1677589768/kenney_impact-sounds.zip#Audio/impactPlate_heavy_004.ogg,CC0-1.0,Kenney (Impact Sounds),loudnorm I=-16/TP=-3+ogg 128kbps,2026-04-28
|
||||
audio/music/defeat.ogg,https://opengameart.org/sites/default/files/Game%20over%20man_0.mp3,CC-BY-4.0,Bogart VGM (OpenGameArt),loudnorm I=-16/TP=-3+mp3→ogg 128kbps,2026-04-28
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 3.
|
|
|
@ -60,6 +60,13 @@
|
|||
"bus": "SFX",
|
||||
"description": "Soft footstep tick on unit movement (throttled)."
|
||||
},
|
||||
"defeat_stinger": {
|
||||
"stream": "audio/sfx/defeat_stinger.ogg",
|
||||
"volume_db": -3.0,
|
||||
"bus": "SFX",
|
||||
"fallback": "_silent",
|
||||
"description": "Somber descending stinger on defeat / player_eliminated."
|
||||
},
|
||||
"victory_fanfare": {
|
||||
"stream": "audio/sfx/victory_fanfare.ogg",
|
||||
"volume_db": -2.0,
|
||||
|
|
@ -418,6 +425,16 @@
|
|||
"era_range": null,
|
||||
"mood": "triumph",
|
||||
"description": "Victory screen music, plays once on victory_achieved."
|
||||
},
|
||||
{
|
||||
"id": "defeat",
|
||||
"stream": "audio/music/defeat.ogg",
|
||||
"volume_db": -6.0,
|
||||
"bus": "Music",
|
||||
"loop": false,
|
||||
"era_range": null,
|
||||
"mood": "lament",
|
||||
"description": "Defeat screen music, plays once when the human player is eliminated."
|
||||
}
|
||||
],
|
||||
"crossfade_seconds": 2.0,
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@
|
|||
{ "type": "array", "items": { "type": "integer", "minimum": 1, "maximum": 10 }, "minItems": 2, "maxItems": 2 }
|
||||
]
|
||||
},
|
||||
"mood": { "type": "string", "enum": ["ambient", "tension", "climactic", "triumph"] },
|
||||
"mood": { "type": "string", "enum": ["ambient", "tension", "climactic", "triumph", "lament"] },
|
||||
"description": { "type": "string" }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ func _connect_event_bus() -> void:
|
|||
EventBus.culture_researched.connect(_on_culture_researched)
|
||||
EventBus.wild_creature_spawned.connect(_on_wild_creature_spawned)
|
||||
EventBus.weather_event_applied.connect(_on_weather_event)
|
||||
EventBus.player_eliminated.connect(_on_player_eliminated)
|
||||
|
||||
|
||||
func _build_music_players() -> void:
|
||||
|
|
@ -537,6 +538,20 @@ func _on_victory_achieved(_player_index: int, _victory_type: String) -> void:
|
|||
play_music("victory")
|
||||
|
||||
|
||||
## Defeat is the human-player counterpart of victory_achieved. The signal
|
||||
## fires for any eliminated player; we only swap to defeat audio when the
|
||||
## eliminated player is the local human, otherwise the listener gets
|
||||
## defeat music for an AI's loss which is wrong.
|
||||
func _on_player_eliminated(player_index: int) -> void:
|
||||
if player_index < 0 or player_index >= GameState.players.size():
|
||||
return
|
||||
var player: RefCounted = GameState.players[player_index] as RefCounted
|
||||
if player == null or not ("is_human" in player) or not bool(player.get("is_human")):
|
||||
return
|
||||
play_sfx("defeat_stinger")
|
||||
play_music("defeat")
|
||||
|
||||
|
||||
func _music_track_for_era(era: int) -> String:
|
||||
for id: String in _music_tracks:
|
||||
var track: Dictionary = _music_tracks[id]
|
||||
|
|
|
|||
10
tools/audio-batch-05-defeat.tsv
Normal file
10
tools/audio-batch-05-defeat.tsv
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Batch 05 — Defeat audio.
|
||||
#
|
||||
# defeat_stinger: short SFX cue — borrowed from Kenney Impact Sounds
|
||||
# (deep heavy plate hit). CC0.
|
||||
# defeat music: "Game over man" by Bogart VGM, CC-BY 4.0
|
||||
# pack page: https://opengameart.org/content/gb-victory-stinger-and-game-over-man
|
||||
# licence quoted on page: "CC-BY 4.0"
|
||||
# Attribution required → recorded in attribution column.
|
||||
audio/sfx/defeat_stinger.ogg https://kenney.nl/media/pages/assets/impact-sounds/8aa7b545c9-1677589768/kenney_impact-sounds.zip#Audio/impactPlate_heavy_004.ogg CC0-1.0 Kenney (Impact Sounds) loudnorm I=-16/TP=-3+ogg 128kbps
|
||||
audio/music/defeat.ogg https://opengameart.org/sites/default/files/Game%20over%20man_0.mp3 CC-BY-4.0 Bogart VGM (OpenGameArt) loudnorm I=-16/TP=-3+mp3→ogg 128kbps
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 19.
|
Loading…
Add table
Reference in a new issue