feat(editor): zone audio configuration panel

- Zone manifest gains audio fields: musicTrack, ambienceDay,
  ambienceNight, musicVolume, ambienceVolume
- Serialized to/from zone.json under "audio" key
- Info panel: collapsable "Zone Audio" section with text inputs for
  music/ambience paths and volume sliders
- Preset selector: Elwynn Forest, Durotar, Darkshore, Dungeon, None
- ZoneManifest stored persistently on EditorApp so audio settings
  survive between exports (was recreated each save)
- Custom zones can now specify their own background music and ambient
  soundscapes via zone.json
This commit is contained in:
Kelsi 2026-05-05 15:48:49 -07:00
parent 36dc9ddef7
commit 2136727c68
5 changed files with 76 additions and 1 deletions

View file

@ -997,7 +997,7 @@ void EditorApp::exportZone(const std::string& outputDir) {
// Write zone manifest (for client loading)
// Scan output directory for all exported tiles (includes adjacent tiles)
ZoneManifest manifest;
ZoneManifest& manifest = zoneManifest_;
manifest.mapName = loadedMap_;
manifest.displayName = loadedMap_;
manifest.tiles.push_back({loadedTileX_, loadedTileY_});

View file

@ -150,6 +150,7 @@ private:
float autoSaveInterval_ = 300.0f;
bool autoSaveEnabled_ = true;
bool showQuitConfirm_ = false;
ZoneManifest zoneManifest_;
// Recent zones
struct RecentZone { std::string mapName; int tileX; int tileY; };
@ -162,6 +163,7 @@ public:
void showToast(const std::string& msg, float duration = 3.0f);
const std::vector<Toast>& getToasts() const { return toasts_; }
const std::vector<RecentZone>& getRecentZones() const { return recentZones_; }
ZoneManifest& getZoneManifest() { return zoneManifest_; }
bool isAutoSaveEnabled() const { return autoSaveEnabled_; }
void setAutoSaveEnabled(bool v) { autoSaveEnabled_ = v; }
float getAutoSaveInterval() const { return autoSaveInterval_; }

View file

@ -2473,6 +2473,51 @@ void EditorUI::renderPropertiesPanel(EditorApp& app) {
ImGui::Text("Height: %.0f-%.0f (avg %.0f)", minH, maxH, sumH / count);
}
// Zone audio configuration
if (app.hasTerrainLoaded() && ImGui::CollapsingHeader("Zone Audio")) {
auto& manifest = app.getZoneManifest();
static char musicBuf[256] = {};
static char ambDayBuf[256] = {};
static char ambNightBuf[256] = {};
std::strncpy(musicBuf, manifest.musicTrack.c_str(), sizeof(musicBuf) - 1);
std::strncpy(ambDayBuf, manifest.ambienceDay.c_str(), sizeof(ambDayBuf) - 1);
std::strncpy(ambNightBuf, manifest.ambienceNight.c_str(), sizeof(ambNightBuf) - 1);
if (ImGui::InputText("Music##audio", musicBuf, sizeof(musicBuf)))
manifest.musicTrack = musicBuf;
if (ImGui::InputText("Ambience (Day)##audio", ambDayBuf, sizeof(ambDayBuf)))
manifest.ambienceDay = ambDayBuf;
if (ImGui::InputText("Ambience (Night)##audio", ambNightBuf, sizeof(ambNightBuf)))
manifest.ambienceNight = ambNightBuf;
ImGui::SliderFloat("Music Vol", &manifest.musicVolume, 0.0f, 1.0f, "%.2f");
ImGui::SliderFloat("Ambience Vol", &manifest.ambienceVolume, 0.0f, 1.0f, "%.2f");
if (ImGui::BeginCombo("Presets##audioPreset", "Select...")) {
if (ImGui::Selectable("Elwynn Forest")) {
manifest.musicTrack = "Sound\\Music\\ZoneMusic\\Forest\\ForestDay.mp3";
manifest.ambienceDay = "Sound\\Ambience\\GlueScreenAmbience.wav";
}
if (ImGui::Selectable("Durotar")) {
manifest.musicTrack = "Sound\\Music\\ZoneMusic\\Barrens\\BarrensDay.mp3";
manifest.ambienceDay = "Sound\\Ambience\\GlueScreenAmbience.wav";
}
if (ImGui::Selectable("Darkshore")) {
manifest.musicTrack = "Sound\\Music\\ZoneMusic\\Darkshore\\DarkshoreDay.mp3";
manifest.ambienceDay = "Sound\\Ambience\\GlueScreenAmbience.wav";
}
if (ImGui::Selectable("Dungeon")) {
manifest.musicTrack = "Sound\\Music\\ZoneMusic\\Dungeon\\DungeonAmbience.mp3";
manifest.ambienceDay = "";
}
if (ImGui::Selectable("None (silent)")) {
manifest.musicTrack = "";
manifest.ambienceDay = "";
manifest.ambienceNight = "";
}
ImGui::EndCombo();
}
}
if (app.getTerrainEditor().hasUnsavedChanges())
ImGui::TextColored(ImVec4(1, 0.8f, 0.3f, 1), "* Unsaved (Ctrl+S to save)");
else

View file

@ -44,6 +44,17 @@ bool ZoneManifest::save(const std::string& path) const {
if (hasCreatures) files["creatures"] = "creatures.json";
j["files"] = files;
// Audio configuration
if (!musicTrack.empty() || !ambienceDay.empty()) {
nlohmann::json audio;
if (!musicTrack.empty()) audio["music"] = musicTrack;
if (!ambienceDay.empty()) audio["ambienceDay"] = ambienceDay;
if (!ambienceNight.empty()) audio["ambienceNight"] = ambienceNight;
audio["musicVolume"] = musicVolume;
audio["ambienceVolume"] = ambienceVolume;
j["audio"] = audio;
}
std::ofstream f(path);
if (!f) { LOG_ERROR("Failed to write zone manifest: ", path); return false; }
f << j.dump(2) << "\n";
@ -76,6 +87,16 @@ bool ZoneManifest::load(const std::string& path) {
}
}
// Audio configuration
if (j.contains("audio")) {
const auto& a = j["audio"];
musicTrack = a.value("music", "");
ambienceDay = a.value("ambienceDay", "");
ambienceNight = a.value("ambienceNight", "");
musicVolume = a.value("musicVolume", 0.7f);
ambienceVolume = a.value("ambienceVolume", 0.5f);
}
return !mapName.empty();
} catch (const std::exception& e) {
LOG_ERROR("Failed to parse zone manifest: ", e.what());

View file

@ -17,6 +17,13 @@ struct ZoneManifest {
bool hasCreatures = false;
std::string description;
// Audio configuration
std::string musicTrack; // Background music file path
std::string ambienceDay; // Daytime ambient sound
std::string ambienceNight; // Nighttime ambient sound
float musicVolume = 0.7f;
float ambienceVolume = 0.5f;
bool save(const std::string& path) const;
bool load(const std::string& path);
};