Skip to content
Faraj edited this page Oct 8, 2026 · 1 revision

Description

The game's music changes with what the player is doing rather than looping one track everywhere. A calm theme plays in the hub and in any room that has been cleared, a battle theme plays while a dungeon room still has enemies in it, and the final boss has a theme of its own. Clearing a room ends the fight music immediately, without waiting for the player to walk out.

Before this, one placeholder track (BGM_03_mp3.mp3) looped in every room including the hub. That track is no longer loaded or played.

The tracks

Track File Plays
Hub theme sounds/lobby_music.mp3 The hub, and any dungeon room with nothing left alive in it
Battle theme sounds/fight_music.mp3 A dungeon room that still has enemies
Boss theme sounds/boss_music.mp3 The final dungeon, while the boss is alive

The hub and battle themes were composed for this project. The boss theme is track 17 of xDeviruchi's 16-Bit Fantasy & Adventure Music, used under its licence. Provenance and the credit that licence requires are recorded in assets/licenses/AUDIO-ATTRIBUTION.md.

Implementation

RoomAssets owns every music decision. It loads all three tracks, and playMusicFor(dungeonId, cleared) picks one:

String wanted = dungeonId == null || cleared
    ? CALM_MUSIC
    : DUNGEON_MUSIC.getOrDefault(dungeonId, FIGHT_MUSIC);

Two details are deliberate. 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. And a dungeon with a theme of its own is one entry in DUNGEON_MUSIC rather than a new branch, so giving another dungeon its own track is a one-line change.

Loading no longer starts anything. Which track belongs depends on the room, so a track is only chosen once a room says where the player is.

Knowing which room the player is in

RoomManager fires roomEntered on the player entity, carrying the room id, its dungeon id and whether the room is cleared. It is fired on the player because the player outlives rooms. MainGameScreen listens and calls playMusicFor.

The same event is re-fired from onRoomCleared(), so beating the last enemy switches the music in place rather than on the next room change. Reusing the event this way means there is no second piece of state to keep in sync — the music simply reacts to the room being re-described.

Not restarting what is already playing

playMusicFor returns early when the track it wants is already playing. Without this, every door between two contested rooms of the same dungeon would restart the battle theme from its opening bar. Switching tracks stops only the tracks being replaced, never the incoming one.

Volume

Volume is not fixed when a track starts. applyMusicVolume() is called every frame from MainGameScreen.render() and sets every loaded track to AudioLevels.music(), which is the settings slider after the mute-when-unfocused rule. Moving the slider or clicking away from the window therefore takes effect immediately, on whichever track the current room happens to be playing.

Looping

Tracks are set to loop with libGDX's Music.setLooping(true), which restarts a track from its beginning. libGDX has no concept of a loop point, so a track with an intro replays that intro on every repeat. This only matters for the boss theme, whose first 13.8 seconds are an intro outside its intended loop, and only in a fight long enough to reach the end of a 4:32 track. It is documented in AUDIO-ATTRIBUTION.md rather than worked around.

Unit Tests

RoomAssetsTest covers the whole system in ten tests: that loading alone never picks a track, that the right track starts for the hub, for a contested dungeon room and for the final dungeon, that clearing a room returns to the calm theme, that the hub stays calm whatever its cleared state claims, that an already-playing track is never restarted, that every loaded track follows the saved volume, that a volume change before loading cannot crash, and that disposal stops everything and unloads it.

RoomAssets is at 100% line coverage and 90.9% branch coverage. See the Music Test Plan for the full tables and the manual checks.

How to Test

Run the game. The hub theme should begin as the player appears.

Hub. Talk to an NPC and open a menu — the hub theme keeps playing through the dialogue and the frozen world. Enter a dungeon and come back, and the hub theme returns.

Dungeons. Walking into a room with enemies starts the battle theme. Open the debug terminal (F1) and type room clear to empty the room without fighting: the battle theme stops and the calm theme returns immediately. Walk between two contested rooms of one dungeon and the battle theme continues rather than restarting. Re-enter a room already cleared and it stays calm.

Boss. Entering the final boss room plays the boss theme instead of the ordinary battle theme, and it holds across all three of the boss's stages because the boss remains one entity throughout. Beating it returns to the calm theme.

Volume. Open settings mid-run and move the music slider; the volume changes immediately on whichever track is playing. Enable "mute when unfocused" and click away from the window; the music silences and returns on focus.

AI Declaration

This page was written with LLM assistance (Claude Code on Opus 5), generated from the music implementation and its test suite, and reviewed before publishing.

Table of Contents

Home

Test Plan

Team 2

Features

Components
Player
Items and Inventory
Enemies
Interactable NPCs
Cutscenes and Dialogue
Music
Bosses
Rooms
Weapons
Abilities & Spells
Game Screens and Areas
Dungeons

Game Engine

Getting Started

Entities and Components

Service Locator

Loading Resources

Logging

Unit Testing

Debug Terminal

Input Handling

UI

Animations

Audio

AI

Physics

Terrain

Concurrency & Threading

Settings

Troubleshooting

MacOS Setup Guide

Clone this wiki locally