Repository navigation
Music Test Plan
This test plan covers the game's music system:
- Per-room track selection — the hub theme, the battle theme and the final boss theme (Music).
- The
roomEnteredcontract that tells the music which room the player is standing in, and re-fires when a room is cleared. - Volume, which follows the settings slider and the mute-when-unfocused rule rather than a fixed level.
- Loading and disposal of the three tracks.
Before this sprint one placeholder track looped in every room, including the hub. Music is now chosen by what the player is doing: the calm hub theme whenever there is nothing left to fight, the battle theme while a dungeon room still has enemies, and the boss theme in the final dungeon.
Testing is divided into automated unit tests (what the code must do) and manual gameplay tests (what the player must hear). Every behaviour listed in the automated plan is enforced by a test in the repository — the test class is named above each table.
-
Unit testing: JUnit 5 and Mockito, run headlessly with the engine's
GameExtension - Game testing: Desktop application
-
Movement:
WASD— Interact:E -
Debug terminal (
F1) commands:room clearto empty the current room without fighting, which is the fastest way to hear the battle-to-calm transition -
Settings: the music volume slider and the "mute when unfocused" option both feed
AudioLevels.music(), which the music system reads every frame
Run all automated tests with:
cd source
./gradlew :core:testCoverage is written to source/core/build/reports/jacoco/test/html/index.html by:
cd source
./gradlew :core:jacocoTestReportLast verified run: 8 Oct 2026 on task/232-hecate-npc — 2326 tests, 0 failures project-wide, of which 10 tests in 1 class cover music.
Reference: SonarCloud project overview · coverage by component
The badges above are project-wide. The figures below are the music code only.
Figures are from a local JaCoCo run of this branch, which is the same measurement SonarCloud consumes. SonarCloud's own published figures lag by one analysis until this branch is merged.
| File | Lines | Line coverage | Branch coverage |
|---|---|---|---|
rooms/RoomAssets.java |
67 | 100.0% | 90.9% |
RoomAssets owns every music decision, so it is the only file whose coverage is meaningful for this feature. Two other files carry a few lines each:
-
rooms/RoomManager.javafires theroomEnteredevent that drives the music. The class is large and mostly unrelated to music, so its overall figure (52.7% line coverage) is not a useful measure of this feature. -
screens/MainGameScreen.javaholds the one-line listener that connects the two. Screen classes are not unit tested anywhere in this project.
Aggregate for RoomAssets (JaCoCo, source/core/build/reports/jacoco/test/html/index.html):
| Counter | Covered | Missed | Coverage |
|---|---|---|---|
| Instructions | 519 | 0 | 100.0% |
| Branches | 20 | 2 | 90.9% |
| Lines | 67 | 0 | 100.0% |
| Methods | 11 | 0 | 100.0% |
| Class | Tests |
|---|---|
RoomAssetsTest |
10 |
| Total | 10 |
The music system is told the player's situation — which dungeon they are in, and whether the room still has enemies — and picks a track from it. The hub is identified by having no dungeon, not by its room id, so renaming the hub in rooms.json cannot silently break the music. A dungeon with its own theme is one entry in a map rather than a new branch.
| Test | Purpose | Expected Result |
|---|---|---|
shouldLoadAssets |
Load every asset a room needs. | Textures, atlases, music and sounds are all handed to the resource service. |
shouldNotStartMusicUntilARoomAsksForIt |
Load assets without entering a room. | No track is even looked up. Which track plays depends on the room, so loading alone must not pick one. |
shouldStartTheTrackTheRoomAsksFor |
Ask for the hub's music. | The track is set looping, set to the saved volume, and played. |
shouldFightOnlyWhileADungeonRoomStillHasEnemies |
Enter a dungeon room with enemies alive, then clear it. | The battle theme plays while enemies remain; clearing the room stops it and returns to the calm theme. |
shouldTreatTheHubAsCalmEvenIfSomethingClaimsItIsNotCleared |
Ask for hub music with cleared false. |
The calm theme plays regardless. The hub has no enemies, so its state can never force a fight track. |
shouldPlayTheBossThemeInTheFinalDungeon |
Enter the final dungeon with the boss alive, then beat it. | The boss theme plays instead of the ordinary battle theme, and beating the boss returns to calm like any other cleared room. |
shouldNotRestartATrackThatIsAlreadyPlaying |
Ask twice for the track already playing. | Nothing is restarted, so crossing rooms of one dungeon never resets the music to its opening bar. |
shouldUnloadOnDispose |
Dispose the room assets. | Every track is stopped and all four asset groups are unloaded. |
Volume is read live from the settings every frame rather than fixed when a track starts, so moving the slider or unfocusing the window takes effect immediately on whichever track the current room happens to be playing.
| Test | Purpose | Expected Result |
|---|---|---|
shouldKeepEveryLoadedTrackOnTheSavedVolume |
Apply the volume while all three tracks are loaded. | Each track is set to AudioLevels.music(), so the setting reaches whichever one is playing. |
shouldIgnoreVolumeChangesForTracksThatAreNotLoaded |
Apply the volume before the tracks are loaded. | No track is fetched, so a volume change during loading cannot crash the game. |
Setup for every test below: run the game with ./gradlew desktop:run. The debug terminal is F1, and room clear empties the current room without fighting.
| Test | Steps | Expected Result |
|---|---|---|
| Plays on spawn | Start a new game. | The calm hub theme begins as the player appears, with no placeholder track underneath it. |
| Survives interaction | Talk to an NPC in the hub and open a menu. | The hub theme keeps playing through the dialogue and the menu; the frozen world does not silence it. |
| Returns after a run | Enter a dungeon, then come back to the hub. | The hub theme returns. |
| Test | Steps | Expected Result |
|---|---|---|
| Battle theme on entry | Walk into a dungeon room with enemies. | The battle theme replaces the hub theme as the room loads. |
| Calm on clear | Defeat every enemy in the room, or run room clear. |
The battle theme stops and the calm theme returns, without waiting for a room change. |
| No restart between rooms | Walk from one contested room of a dungeon to another. | The battle theme continues from where it was rather than restarting. |
| Cleared rooms stay calm | Re-enter a room already cleared. | The calm theme plays; the battle theme does not start again. |
| Boss theme | Enter the final boss room. | The boss theme plays instead of the ordinary battle theme. |
| Boss theme holds across stages | Fight the boss through all three of its stages. | The boss theme keeps playing; it does not drop to calm between stages. |
| Calm after the boss | Defeat the boss. | The music returns to the calm theme. |
| Test | Steps | Expected Result |
|---|---|---|
| Slider takes effect live | Open settings mid-run and move the music slider. | The volume changes immediately, on whichever track is playing. |
| Mute when unfocused | Enable "mute when unfocused", then click away from the window. | The music silences, and returns when the window is focused again. |
| Applies to every track | Set a distinctive volume in the hub, then enter a dungeon. | The battle theme starts at the same volume rather than reverting to a default. |
| Test | Steps | Expected Result |
|---|---|---|
| Levels match between tracks | Move between the hub and a dungeon room several times. | Neither track is noticeably louder than the other at the same slider setting. |
| Loops cleanly | Stay in one room past the end of its track. | The track loops without an audible gap or a jarring restart. |
Two branches are uncovered, both defensive:
| Location | Uncovered | Why |
|---|---|---|
RoomAssets.playMusicFor() |
The track == null half of the early return |
Guards a track that is not loaded. The resource service is always registered in the game and in every test, so only the loaded path is exercised. |
RoomAssets.music() |
The resourceService == null half of the guard |
Guards a lookup after the service locator is torn down. The containsAsset half of the same condition is covered. |
- The tests assert which
Musicobject is played, looped and stopped. They do not assert that any sound is produced; audio output is verified manually. - Mixing is judged by ear, not asserted. Relative loudness between the three tracks, and whether the battle theme sits well under combat sound effects, are manual checks.
- Loop quality is manual. libGDX restarts a looping track from its beginning and has no concept of a loop point, so a track with an intro replays that intro on every repeat. The boss theme has a 13.8 second intro that is not part of its intended loop; this is documented in
assets/licenses/AUDIO-ATTRIBUTION.mdand is only audible in a fight long enough to loop. -
roomEnteredis fired byRoomManagerand consumed inMainGameScreen. Neither end is unit tested —RoomManager's music announcement is exercised only through gameplay, and screen classes are not unit tested anywhere in this project. The event's payload is simple (room id, dungeon id, cleared) and a wrong value shows up immediately as the wrong track. - Music does not duck or pause for dialogue and cutscenes. The world freezes but the track continues, which was a deliberate choice rather than an untested behaviour.
This page was written with LLM assistance (Claude Code on Opus 5), generated from the music test suite, the JaCoCo report and the project's pull requests, and reviewed before publishing.