From b8d79c7b7b1510bbf68d249b3bf98391f4a26153 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Wed, 24 Jun 2026 20:58:31 +0300 Subject: [PATCH] Working on music cross fade and settings --- audio/bishkek night.ogg | 3 + cmakeaudioplayer/CMakeLists.txt | 36 -- cmakeaudioplayer/examples/test_audio.cpp | 32 -- cmakeaudioplayer/include/AudioPlayer.hpp | 37 -- cmakeaudioplayer/src/AudioPlayer.cpp | 194 --------- .../dialogue/uni_exterior_dialogues.json | 407 ++++++++++++++++++ resources/w/ui/screen_settings.json | 92 ++++ src/AudioPlayerAsync.cpp | 136 +++++- src/AudioPlayerAsync.h | 15 + src/Game.cpp | 10 +- src/MenuManager.cpp | 116 ++++- src/MenuManager.h | 3 + src/UiManager.cpp | 30 +- 13 files changed, 779 insertions(+), 332 deletions(-) create mode 100644 audio/bishkek night.ogg delete mode 100644 cmakeaudioplayer/CMakeLists.txt delete mode 100644 cmakeaudioplayer/examples/test_audio.cpp delete mode 100644 cmakeaudioplayer/include/AudioPlayer.hpp delete mode 100644 cmakeaudioplayer/src/AudioPlayer.cpp create mode 100644 resources/dialogue/uni_exterior_dialogues.json diff --git a/audio/bishkek night.ogg b/audio/bishkek night.ogg new file mode 100644 index 0000000..ed6c6fa --- /dev/null +++ b/audio/bishkek night.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:773637b833576d7075403a8f276551bebd168ac105594dc2e248996039f51551 +size 4204745 diff --git a/cmakeaudioplayer/CMakeLists.txt b/cmakeaudioplayer/CMakeLists.txt deleted file mode 100644 index dc65eaf..0000000 --- a/cmakeaudioplayer/CMakeLists.txt +++ /dev/null @@ -1,36 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(AudioPlayer) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Use pkg-config to find Vorbis -#find_package(PkgConfig REQUIRED) -#pkg_check_modules(VORBIS REQUIRED vorbis vorbisfile) -#pkg_check_modules(OGG REQUIRED ogg) -find_package(OpenAL REQUIRED) - -add_library(audioplayer - src/AudioPlayer.cpp - include/AudioPlayer.hpp -) - -target_include_directories(audioplayer - PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include - ${OPENAL_INCLUDE_DIR} - ${VORBIS_INCLUDE_DIRS} - ${OGG_INCLUDE_DIRS} -) - -target_link_libraries(audioplayer - PUBLIC - ${OPENAL_LIBRARY} - ${VORBIS_LIBRARIES} - ${OGG_LIBRARIES} -) - -# Test executable -add_executable(test_audio examples/test_audio.cpp) -target_link_libraries(test_audio PRIVATE audioplayer stdc++fs) -#git add ../../sounds diff --git a/cmakeaudioplayer/examples/test_audio.cpp b/cmakeaudioplayer/examples/test_audio.cpp deleted file mode 100644 index 06635d6..0000000 --- a/cmakeaudioplayer/examples/test_audio.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "AudioPlayer.hpp" -#include -#include -#include -#include - -int main() { - try { - AudioPlayer player; - const std::string filename = "Symphony No.6 (1st movement).ogg"; - - std::cout << "🔍 Looking for file: " << filename << " in sounds directory...\n"; - - if (!player.playFromSoundsDir(filename)) { - std::cout << "❌ Failed to play audio file\n"; - return 1; - } - - std::cout << "✅ Playing symphony...\n"; - - // Check status for 30 seconds - for (int i = 0; i < 30; ++i) { - std::cout << "📊 Status: " << (player.isPlaying() ? "Playing ▶️" : "Stopped ⏹️") << "\n"; - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - - return 0; - } catch (const std::exception& e) { - std::cerr << "❌ Error: " << e.what() << "\n"; - return 1; - } -} diff --git a/cmakeaudioplayer/include/AudioPlayer.hpp b/cmakeaudioplayer/include/AudioPlayer.hpp deleted file mode 100644 index b1d23f1..0000000 --- a/cmakeaudioplayer/include/AudioPlayer.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -class AudioPlayer { -public: - AudioPlayer(); - ~AudioPlayer(); - - // Для музыки с зацикливанием (если filename пустой - продолжает играть текущую) - bool playMusic(const std::string& filename = ""); - - // Для одноразовых звуковых эффектов - bool playSound(const std::string& filename); - - void stop(); - bool isPlaying() const; - -private: - ALCdevice* device; - ALCcontext* context; - ALuint musicSource; // Источник для музыки - ALuint soundSource; // Источник для звуков - ALuint musicBuffer; // Буфер для музыки - ALuint soundBuffer; // Буфер для звуков - bool playing; - std::string currentMusic; // Хранит имя текущего музыкального файла - - std::vector loadOgg(const std::string& filename, ALuint buffer); - std::string findFileInSounds(const std::string& filename); - bool isOggFile(const std::string& filename) const; -}; diff --git a/cmakeaudioplayer/src/AudioPlayer.cpp b/cmakeaudioplayer/src/AudioPlayer.cpp deleted file mode 100644 index d13ca6f..0000000 --- a/cmakeaudioplayer/src/AudioPlayer.cpp +++ /dev/null @@ -1,194 +0,0 @@ -#include "AudioPlayer.hpp" -#include -#include -#include -#include -#include -#include - -AudioPlayer::AudioPlayer() : device(nullptr), context(nullptr), - musicSource(0), soundSource(0), musicBuffer(0), soundBuffer(0), playing(false) { - device = alcOpenDevice(nullptr); - if (!device) { - throw std::runtime_error("Failed to open audio device"); - } - - context = alcCreateContext(device, nullptr); - if (!context) { - alcCloseDevice(device); - throw std::runtime_error("Failed to create audio context"); - } - - alcMakeContextCurrent(context); - alGenSources(1, &musicSource); - alGenSources(1, &soundSource); - alGenBuffers(1, &musicBuffer); - alGenBuffers(1, &soundBuffer); -} - -AudioPlayer::~AudioPlayer() { - if (musicSource) - alDeleteSources(1, &musicSource); - if (soundSource) - alDeleteSources(1, &soundSource); - if (musicBuffer) - alDeleteBuffers(1, &musicBuffer); - if (soundBuffer) - alDeleteBuffers(1, &soundBuffer); - - if (context) { - alcMakeContextCurrent(nullptr); - alcDestroyContext(context); - } - if (device) - alcCloseDevice(device); -} - -bool AudioPlayer::isOggFile(const std::string& filename) const { - std::string ext = std::filesystem::path(filename).extension().string(); - std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); - return ext == ".ogg"; -} - -std::string AudioPlayer::findFileInSounds(const std::string& filename) { - // Primary search path - "sounds" directory next to executable - std::filesystem::path soundsDir = std::filesystem::current_path() / "sounds"; - - // Alternative search paths - std::vector altPaths = { - std::filesystem::current_path() / ".." / "sounds", // One level up - std::filesystem::current_path() / ".." / ".." / "sounds", // Two levels up - "/home/albert/gay-jam/ZeptoLabTest1/sounds" // Absolute path - }; - - std::cout << "🔍 Searching for \"" << filename << "\" in:\n"; - std::cout << " " << soundsDir << "\n"; - - if (std::filesystem::exists(soundsDir / filename)) { - return (soundsDir / filename).string(); - } - - // Try alternative paths - for (const auto& path : altPaths) { - std::cout << " " << path << "\n"; - if (std::filesystem::exists(path / filename)) { - return (path / filename).string(); - } - } - - throw std::runtime_error("❌ File not found: " + filename); -} - -std::vector AudioPlayer::loadOgg(const std::string& filename, ALuint buffer) { - FILE* file = fopen(filename.c_str(), "rb"); - if (!file) { - throw std::runtime_error("Cannot open file: " + filename); - } - - OggVorbis_File vf; - if (ov_open_callbacks(file, &vf, nullptr, 0, OV_CALLBACKS_DEFAULT) < 0) { - fclose(file); - throw std::runtime_error("Input not an Ogg file: " + filename); - } - - vorbis_info* vi = ov_info(&vf, -1); - std::vector audioData; - char data[4096]; - int bitstream; - long bytes; - - do { - bytes = ov_read(&vf, data, sizeof(data), 0, 2, 1, &bitstream); - if (bytes > 0) { - audioData.insert(audioData.end(), data, data + bytes); - } - } while (bytes > 0); - - // Setup the buffer with the audio data - alBufferData(buffer, - (vi->channels == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, - audioData.data(), - audioData.size(), - vi->rate); - - ov_clear(&vf); - return audioData; -} - -bool AudioPlayer::playMusic(const std::string& filename) { - try { - // Если filename пустой, просто проверяем играет ли музыка - if (filename.empty()) { - if (!isPlaying()) { - alSourcei(musicSource, AL_LOOPING, AL_TRUE); // Включаем зацикливание - alSourcePlay(musicSource); - } - return true; - } - - // Если filename не пустой, загружаем новую музыку - if (!isOggFile(filename)) { - std::cerr << "❌ Error: Music file must be an .ogg file\n"; - return false; - } - - std::string fullPath = findFileInSounds(filename); - std::cout << "✅ Found music file: " << fullPath << "\n"; - - // Останавливаем текущую музыку - alSourceStop(musicSource); - - // Загружаем и настраиваем новую музыку - loadOgg(fullPath, musicBuffer); - alSourcei(musicSource, AL_BUFFER, musicBuffer); - alSourcei(musicSource, AL_LOOPING, AL_TRUE); // Включаем зацикливание - - std::cout << "▶️ Starting music playback... " << musicSource << std::endl; - std::cout << "▶️ Music buffer... " << musicBuffer << std::endl; - alSourcePlay(musicSource); - - currentMusic = filename; - playing = true; - return true; - } catch (const std::exception& e) { - std::cerr << "❌ Error playing music: " << e.what() << std::endl; - return false; - } -} - -bool AudioPlayer::playSound(const std::string& filename) { - try { - if (!isOggFile(filename)) { - std::cerr << "❌ Error: Sound file must be an .ogg file\n"; - return false; - } - - std::string fullPath = findFileInSounds(filename); - std::cout << "✅ Found sound file: " << fullPath << "\n"; - - // Загружаем и настраиваем звук - loadOgg(fullPath, soundBuffer); - alSourcei(soundSource, AL_BUFFER, soundBuffer); - alSourcei(soundSource, AL_LOOPING, AL_FALSE); // Выключаем зацикливание - - std::cout << "▶️ Playing sound effect...\n"; - alSourcePlay(soundSource); - - return true; - } catch (const std::exception& e) { - std::cerr << "❌ Error playing sound: " << e.what() << std::endl; - return false; - } -} - -void AudioPlayer::stop() { - alSourceStop(musicSource); - alSourceStop(soundSource); - playing = false; -} - -bool AudioPlayer::isPlaying() const { - ALint state; - alGetSourcei(musicSource, AL_SOURCE_STATE, &state); - return state == AL_PLAYING; -} diff --git a/resources/dialogue/uni_exterior_dialogues.json b/resources/dialogue/uni_exterior_dialogues.json new file mode 100644 index 0000000..e49b048 --- /dev/null +++ b/resources/dialogue/uni_exterior_dialogues.json @@ -0,0 +1,407 @@ +{ + "dialogues": [ + { + "id": "door_dialog001", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "Дверь закрыта на ключ.", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_contaier001", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "Это куча строительного мусора, я не буду в ней копаться!", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_contaier003", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "Я уже забрал курсовую работу Бегимай, больше мне незачем сюда лезть.", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_contaier002", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "Если Алик не соврал, курсовая работа Бегимай лежит где-то тут.", + "next": "line_2" + }, + { + "id": "line_2", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "[Копается в куче мусора]", + "next": "line_3" + }, + { + "id": "line_3", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "Ага, нашел! Вот и она!", + "luaCallback": "dialog_contaier002_get_coursework", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_contaier003", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "Я уже нашел в этой куче мусора то, что мне нужно.", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_taxi001", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "Прежде чем выходить за ворота, я должен заказать такси до общаги.", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_taxi002", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "Я заказал такси до общаги, машина уже ждет!", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_taxi004", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "Я уже заказал такси, машина уже ждет!", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_video001", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "Ого, пока я залипал в приложении, уже наступила ночь!", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_video002", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "Я не буду сейчас смотреть видеоролики, давай лучше вернемся в общагу и пойдем спать.", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_video003", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_hero_neutral.png", + "text": "Мне некогда деградировать сегодня, у меня много дел!", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_chat_parents001", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Отец", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "Бекзат, сынок, мы c мамой тебе отправили немного денег, постарайся прожить на эти деньги до конца недели!", + "next": "line_2", + "chatBubble": "in" + }, + { + "id": "line_2", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "Спасибо!", + "next": "end_1", + "chatBubble": "out" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_chat_news001", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Отец", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "Жители Бишкека все чаще жалуются на депрессию и апатию. Смотрите свежее видео об этом на нашем канале!", + "next": "end_1", + "chatBubble": "in" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialog_chat_aiperi001", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Айпери", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "Бекзат, помнишь мы скидывались на торт для Аиды Джаныбековой? Я тогда еще приносила скатерть, тарелки и нож для торта. И я до сих пор не получила назад ничего.", + "next": "line_2", + "chatBubble": "in" + }, + { + "id": "line_2", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "Скатерть и тарелки вроде бы лежат в студзоне.", + "next": "line_3", + "chatBubble": "out" + }, + { + "id": "line_3", + "type": "Line", + "speaker": "Айпери", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "А нож?", + "next": "line_4", + "chatBubble": "in" + }, + { + "id": "line_4", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "Нож, наверное, так и остался в учительской.", + "next": "line_5", + "chatBubble": "out" + }, + { + "id": "line_5", + "type": "Line", + "speaker": "Айпери", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "А давай не \"наверное\"?", + "next": "line_6", + "chatBubble": "in" + }, + { + "id": "line_6", + "type": "Line", + "speaker": "Айпери", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "А давай ты приедешь в универ, зайдешь в учительскую, заберешь нож и отдашь мне?", + "next": "line_7", + "chatBubble": "in" + }, + { + "id": "line_7", + "type": "Line", + "speaker": "Айпери", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "У вас сегодня как раз Аида ведет лекцию. После лекции попросишь у нее ключи от учительской и заберешь нож.", + "next": "line_8", + "chatBubble": "in" + }, + { + "id": "line_8", + "type": "Line", + "speaker": "Бекзат", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "Почему ты сама не можешь забрать?", + "next": "line_9", + "chatBubble": "out" + }, + { + "id": "line_9", + "type": "Line", + "speaker": "Айпери", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "Ты же знаешь, если я встречу Аиду, она 100% даст мне какое-нибудь сложное задание.", + "next": "line_10", + "chatBubble": "in" + }, + { + "id": "line_10", + "type": "Line", + "speaker": "Айпери", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "И потом, это ты у меня брал нож, с чего я должна ходить искать его по всему универу?", + "next": "line_11", + "chatBubble": "in" + }, + { + "id": "line_11", + "type": "Line", + "speaker": "Айпери", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "Так что жду тебя в универе! Не вздумай прогулять!", + "next": "setflag_1", + "chatBubble": "in", + "questUnlock": "aiperi_knife" + }, + { + "id": "setflag_1", + "type": "SetFlag", + "effects": [ + { + "flag": "aiperi_knife_aware", + "value": 1 + } + ], + "next": "end_1" + }, + { + "id": "end_1", + "luaCallback": "on_aiperi_dialog_over", + "type": "End" + } + ] + }, + { + "id": "dialog_chat_aiperi002", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Айпери", + "portrait": "resources/dialogue/portrait_phone.png", + "text": "Бекзат, ты где? Я жду тебя возле лестницы.", + "next": "end_1", + "chatBubble": "in" + }, + { + "id": "end_1", + "type": "End" + } + ] +} + ] +} diff --git a/resources/w/ui/screen_settings.json b/resources/w/ui/screen_settings.json index 85ad863..a030aad 100644 --- a/resources/w/ui/screen_settings.json +++ b/resources/w/ui/screen_settings.json @@ -11,6 +11,98 @@ "height": "match_parent", "texture": "resources/w/ui/img/main/bkg.png" }, + { + "type": "LinearLayout", + "orientation": "vertical", + "horizontal_gravity": "center", + "vertical_gravity": "center", + "horizontal_align": "left", + "width": 500, + "height": 600, + "spacing": 20, + "children": [ + { + "type": "TextView", + "name": "settingsTitleText", + "width": 500, + "height": 60, + "text": "Settings", + "fontSize": 40, + "textCentered": true, + "color": [1.0, 1.0, 1.0, 1.0] + }, + { + "type": "TextView", + "name": "musicVolumeText", + "width": 500, + "height": 40, + "text": "Music volume: 100", + "fontSize": 32, + "textCentered": false, + "topAligned": true, + "paddingX": 10, + "color": [1.0, 1.0, 1.0, 1.0] + }, + { + "type": "Slider", + "name": "musicVolumeSlider", + "width": 500, + "height": 40, + "orientation": "horizontal", + "value": 0.78125, + "interactive": true, + "textures": { + "track": "resources/w/red.png", + "knob": "resources/w/blue.png" + } + }, + { + "type": "TextView", + "name": "soundVolumeText", + "width": 500, + "height": 40, + "text": "Sound volume: 80", + "fontSize": 32, + "textCentered": false, + "topAligned": true, + "paddingX": 10, + "color": [1.0, 1.0, 1.0, 1.0] + }, + { + "type": "Slider", + "name": "soundVolumeSlider", + "width": 500, + "height": 40, + "orientation": "horizontal", + "value": 0.625, + "interactive": true, + "textures": { + "track": "resources/w/red.png", + "knob": "resources/w/blue.png" + } + }, + { + "type": "TextButton", + "name": "musicToggleButton", + "width": 500, + "height": 60, + "text": "Music: ON", + "fontSize": 32, + "textCentered": true, + "color": [0.2, 0.9, 0.2, 1.0] + }, + { + "type": "TextButton", + "name": "soundToggleButton", + "width": 500, + "height": 60, + "text": "Sound: ON", + "fontSize": 32, + "textCentered": true, + "color": [0.2, 0.9, 0.2, 1.0] + } + ] + }, { "type": "TextButton", "name": "settingsBackButton", diff --git a/src/AudioPlayerAsync.cpp b/src/AudioPlayerAsync.cpp index b5a6b39..b4dcddc 100644 --- a/src/AudioPlayerAsync.cpp +++ b/src/AudioPlayerAsync.cpp @@ -43,6 +43,12 @@ bool AudioPlayerAsync::init() { void AudioPlayerAsync::shutdown() { if (!initialized) return; + if (currentMusic_) { + Mix_HaltMusic(); + Mix_FreeMusic(currentMusic_); + currentMusic_ = nullptr; + } + { std::lock_guard lock(soundCacheMutex); for (auto& pair : soundCache) { @@ -62,6 +68,7 @@ void AudioPlayerAsync::playSoundAsync(const std::string& filePath, int loops, in std::cerr << "AudioPlayerAsync not initialized" << std::endl; return; } + if (!soundEnabled_) return; auto task = [this, filePath, loops, channel]() { Mix_Chunk* sound = nullptr; @@ -99,20 +106,20 @@ void AudioPlayerAsync::playSoundAsync(const std::string& filePath, int loops, in } void AudioPlayerAsync::playMusicAsync(const std::string& filePath, int loops) { - std::cout << "AudioPlayerAsync::playMusicAsync called step 1" << std::endl; if (!initialized) return; - std::cout << "AudioPlayerAsync::playMusicAsync called step 1" << std::endl; if (filePath == currentTrack) - { return; - } currentTrack = filePath; + if (!musicEnabled_) return; - auto task = [filePath, loops]() { - std::cout << "AudioPlayerAsync::playMusicAsync called step 3" << std::endl; - + auto task = [this, filePath, loops]() { + Mix_HaltMusic(); + if (currentMusic_) { + Mix_FreeMusic(currentMusic_); + currentMusic_ = nullptr; + } Mix_Music* music = Mix_LoadMUS(filePath.c_str()); if (!music) { std::cerr << "Failed to load music " << filePath << ": " << Mix_GetError() << std::endl; @@ -121,7 +128,9 @@ void AudioPlayerAsync::playMusicAsync(const std::string& filePath, int loops) { if (Mix_PlayMusic(music, loops) == -1) { std::cerr << "Mix_PlayMusic failed: " << Mix_GetError() << std::endl; Mix_FreeMusic(music); + return; } + currentMusic_ = music; }; #ifdef __EMSCRIPTEN__ @@ -178,6 +187,7 @@ void AudioPlayerAsync::resumeMusicAsync() { void AudioPlayerAsync::setMusicVolume(int volume) { if (!initialized) return; volume = std::max(0, std::min(128, volume)); + musicVolume_ = volume; #ifdef __EMSCRIPTEN__ Mix_VolumeMusic(volume); #else @@ -192,6 +202,7 @@ void AudioPlayerAsync::setMusicVolume(int volume) { void AudioPlayerAsync::setSoundVolume(int volume) { if (!initialized) return; volume = std::max(0, std::min(128, volume)); + soundVolume_ = volume; #ifdef __EMSCRIPTEN__ Mix_Volume(-1, volume); #else @@ -203,6 +214,117 @@ void AudioPlayerAsync::setSoundVolume(int volume) { #endif } +void AudioPlayerAsync::crossFadeMusicAsync(const std::string& filePath, int fadeDurationMs, int loops) { + if (!initialized) return; + if (filePath == currentTrack) return; + currentTrack = filePath; + if (!musicEnabled_) return; + + auto task = [this, filePath, fadeDurationMs, loops]() { +#ifdef __EMSCRIPTEN__ + Mix_HaltMusic(); + if (currentMusic_) { Mix_FreeMusic(currentMusic_); currentMusic_ = nullptr; } +#else + if (Mix_PlayingMusic()) { + Mix_FadeOutMusic(fadeDurationMs / 2); + SDL_Delay(fadeDurationMs / 2 + 20); + } + Mix_HaltMusic(); + if (currentMusic_) { Mix_FreeMusic(currentMusic_); currentMusic_ = nullptr; } +#endif + Mix_Music* music = Mix_LoadMUS(filePath.c_str()); + if (!music) { + std::cerr << "Failed to load music " << filePath << ": " << Mix_GetError() << std::endl; + return; + } + if (Mix_FadeInMusic(music, loops, fadeDurationMs / 2) == -1) { + std::cerr << "Mix_FadeInMusic failed: " << Mix_GetError() << std::endl; + Mix_FreeMusic(music); + return; + } + currentMusic_ = music; + }; + +#ifdef __EMSCRIPTEN__ + task(); +#else + std::unique_lock lock(mtx); + taskQueue.push(std::move(task)); + cv.notify_one(); +#endif +} + +void AudioPlayerAsync::crossFadeMusicFromPositionAsync(const std::string& filePath, int fadeDurationMs, int loops) { + if (!initialized) return; + if (filePath == currentTrack) return; + currentTrack = filePath; + if (!musicEnabled_) return; + + auto task = [this, filePath, fadeDurationMs, loops]() { + double position = 0.0; + if (currentMusic_ && Mix_PlayingMusic()) { + double pos = Mix_GetMusicPosition(currentMusic_); + if (pos >= 0.0) position = pos; + } +#ifndef __EMSCRIPTEN__ + if (Mix_PlayingMusic()) { + Mix_FadeOutMusic(fadeDurationMs / 2); + SDL_Delay(fadeDurationMs / 2 + 20); + } +#endif + Mix_HaltMusic(); + if (currentMusic_) { Mix_FreeMusic(currentMusic_); currentMusic_ = nullptr; } + + Mix_Music* music = Mix_LoadMUS(filePath.c_str()); + if (!music) { + std::cerr << "Failed to load music " << filePath << ": " << Mix_GetError() << std::endl; + return; + } + if (Mix_FadeInMusicPos(music, loops, fadeDurationMs / 2, position) == -1) { + std::cerr << "Mix_FadeInMusicPos failed: " << Mix_GetError() << std::endl; + Mix_FreeMusic(music); + return; + } + currentMusic_ = music; + }; + +#ifdef __EMSCRIPTEN__ + task(); +#else + std::unique_lock lock(mtx); + taskQueue.push(std::move(task)); + cv.notify_one(); +#endif +} + +void AudioPlayerAsync::setMusicEnabled(bool enabled) { + if (musicEnabled_ == enabled) return; + musicEnabled_ = enabled; + if (!initialized) return; + + if (!enabled) { + // Halt playback but keep currentTrack so we can restart it on re-enable. +#ifdef __EMSCRIPTEN__ + Mix_HaltMusic(); +#else + std::unique_lock lock(mtx); + taskQueue.push([]() { Mix_HaltMusic(); }); + cv.notify_one(); +#endif + } else { + // Restart the last requested track, if any. + if (!currentTrack.empty()) { + const std::string track = currentTrack; + currentTrack = ""; // clear so crossFadeMusicAsync doesn't bail early + crossFadeMusicAsync(track); + } + } +} + +void AudioPlayerAsync::setSoundEnabled(bool enabled) { + soundEnabled_ = enabled; +} + #ifndef __EMSCRIPTEN__ void AudioPlayerAsync::workerThread() { while (true) { diff --git a/src/AudioPlayerAsync.h b/src/AudioPlayerAsync.h index 4ae1fd5..1f2fe07 100644 --- a/src/AudioPlayerAsync.h +++ b/src/AudioPlayerAsync.h @@ -24,11 +24,20 @@ public: void playSoundAsync(const std::string& filePath, int loops = 0, int channel = -1); void playMusicAsync(const std::string& filePath, int loops = -1); + void crossFadeMusicAsync(const std::string& filePath, int fadeDurationMs = 1000, int loops = -1); + void crossFadeMusicFromPositionAsync(const std::string& filePath, int fadeDurationMs = 1000, int loops = -1); void stopMusicAsync(); void pauseMusicAsync(); void resumeMusicAsync(); void setMusicVolume(int volume); // 0..128 void setSoundVolume(int volume); // 0..128 + int getMusicVolume() const { return musicVolume_; } + int getSoundVolume() const { return soundVolume_; } + + void setMusicEnabled(bool enabled); + void setSoundEnabled(bool enabled); + bool isMusicEnabled() const { return musicEnabled_; } + bool isSoundEnabled() const { return soundEnabled_; } void exit() { stop = true; } @@ -47,6 +56,12 @@ private: std::mutex soundCacheMutex; std::string currentTrack; + Mix_Music* currentMusic_ = nullptr; + + int musicVolume_ = 128; + int soundVolume_ = 128; + bool musicEnabled_ = true; + bool soundEnabled_ = true; bool initialized = false; }; \ No newline at end of file diff --git a/src/Game.cpp b/src/Game.cpp index 96695de..7369c33 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -327,7 +327,7 @@ namespace ZL menuManager.startGameFunc = [this]() { gameState.currentLocationName = "location_dorm"; currentLocation()->scriptEngine.callLocationEnterCallback(); - this->audioPlayer->playMusicAsync("audio/obshaga.ogg"); + this->audioPlayer->crossFadeMusicAsync("audio/obshaga.ogg"); }; menuManager.onSaveGame = [this](int slot) { saveGame(slot); }; @@ -343,7 +343,9 @@ namespace ZL std::cout << "Audio initialization failed" << std::endl; } - audioPlayer->playMusicAsync("audio/main menu final.ogg"); + menuManager.loadSettings(); + + audioPlayer->crossFadeMusicAsync("audio/main menu final.ogg"); loadingCompleted = true; @@ -453,10 +455,10 @@ namespace ZL } gameState.locations["uni_interior"]->requestDarklandsPlayBattleMusic = [this]() { - audioPlayer->playMusicAsync("audio/bishkek fight.ogg"); + audioPlayer->crossFadeMusicFromPositionAsync("audio/bishkek fight.ogg"); }; gameState.locations["uni_interior"]->requestDarklandsPlayNormalMusic = [this]() { - audioPlayer->playMusicAsync("audio/bishkek fight calm.ogg"); + audioPlayer->crossFadeMusicFromPositionAsync("audio/bishkek fight calm.ogg"); }; LocationSetup uniExteriorParams = uniInteriorParams; uniExteriorParams.gameObjectsJsonPath = "resources/config2/gameobjects_uni_exterior_x.json"; diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index d90c46b..c81af3e 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -1,7 +1,9 @@ #include "MenuManager.h" #include "Game.h" #include "render/TextRenderer.h" +#include "utils/Utils.h" #include +#include #include #include @@ -252,7 +254,7 @@ namespace ZL { void MenuManager::showMainMenu() { if (onResetGame) onResetGame(); - audioPlayer_.playMusicAsync("audio/main menu final.ogg"); + audioPlayer_.crossFadeMusicAsync("audio/main menu final.ogg"); uiState_ = GameUiState::MainMenu; uiManager.clearMenuStack(); @@ -294,9 +296,72 @@ namespace ZL { void MenuManager::showSettingsScreen() { uiManager.pushMenuFromSavedRoot(settingsScreenRoot); uiManager.setTextButtonCallback("settingsBackButton", [this](const std::string&) { + saveSettings(); uiManager.popMenu(); uiManager.updateAllLayouts(); + }); + + // Set initial slider positions before registering callbacks so + // setSliderValue does not fire an unregistered callback. + const float musicFrac = audioPlayer_.getMusicVolume() / 128.0f; + const float soundFrac = audioPlayer_.getSoundVolume() / 128.0f; + uiManager.setSliderValue("musicVolumeSlider", musicFrac); + uiManager.setSliderValue("soundVolumeSlider", soundFrac); + uiManager.setText("musicVolumeText", + "Music volume: " + std::to_string(audioPlayer_.getMusicVolume())); + uiManager.setText("soundVolumeText", + "Sound volume: " + std::to_string(audioPlayer_.getSoundVolume())); + + uiManager.setSliderCallback("musicVolumeSlider", + [this](const std::string&, float value) { + const int vol = static_cast(value * 128.0f + 0.5f); + audioPlayer_.setMusicVolume(vol); + uiManager.setText("musicVolumeText", + "Music volume: " + std::to_string(vol)); }); + uiManager.setSliderCallback("soundVolumeSlider", + [this](const std::string&, float value) { + const int vol = static_cast(value * 128.0f + 0.5f); + audioPlayer_.setSoundVolume(vol); + uiManager.setText("soundVolumeText", + "Sound volume: " + std::to_string(vol)); + }); + + // Music on/off toggle — helper to sync button text and color from current state + auto refreshMusicToggle = [this]() { + const bool on = audioPlayer_.isMusicEnabled(); + uiManager.setTextButtonText("musicToggleButton", on ? "Music: ON" : "Music: OFF"); + uiManager.setTextButtonColor("musicToggleButton", on + ? std::array{ 0.2f, 0.9f, 0.2f, 1.0f } + : std::array{ 0.9f, 0.2f, 0.2f, 1.0f }); + }; + auto refreshSoundToggle = [this]() { + const bool on = audioPlayer_.isSoundEnabled(); + uiManager.setTextButtonText("soundToggleButton", on ? "Sound: ON" : "Sound: OFF"); + uiManager.setTextButtonColor("soundToggleButton", on + ? std::array{ 0.2f, 0.9f, 0.2f, 1.0f } + : std::array{ 0.9f, 0.2f, 0.2f, 1.0f }); + }; + + refreshMusicToggle(); + refreshSoundToggle(); + + uiManager.setTextButtonCallback("musicToggleButton", [this](const std::string&) { + audioPlayer_.setMusicEnabled(!audioPlayer_.isMusicEnabled()); + const bool on = audioPlayer_.isMusicEnabled(); + uiManager.setTextButtonText("musicToggleButton", on ? "Music: ON" : "Music: OFF"); + uiManager.setTextButtonColor("musicToggleButton", on + ? std::array{ 0.2f, 0.9f, 0.2f, 1.0f } + : std::array{ 0.9f, 0.2f, 0.2f, 1.0f }); + }); + uiManager.setTextButtonCallback("soundToggleButton", [this](const std::string&) { + audioPlayer_.setSoundEnabled(!audioPlayer_.isSoundEnabled()); + const bool on = audioPlayer_.isSoundEnabled(); + uiManager.setTextButtonText("soundToggleButton", on ? "Sound: ON" : "Sound: OFF"); + uiManager.setTextButtonColor("soundToggleButton", on + ? std::array{ 0.2f, 0.9f, 0.2f, 1.0f } + : std::array{ 0.9f, 0.2f, 0.2f, 1.0f }); + }); } void MenuManager::showLoadGameScreen() @@ -906,29 +971,29 @@ namespace ZL { setupGameplayHudCallbacks(); if (gameState_.isDarklands) { - audioPlayer_.playMusicAsync("audio/bishkek fight calm.ogg"); + audioPlayer_.crossFadeMusicAsync("audio/bishkek fight calm.ogg"); } else if (gameState_.isNight) { - audioPlayer_.playMusicAsync("audio/bishkek fight calm.ogg"); + audioPlayer_.crossFadeMusicAsync("audio/bishkek night.ogg"); } else { - audioPlayer_.playMusicAsync("audio/bishkek univer day.ogg"); + audioPlayer_.crossFadeMusicAsync("audio/bishkek univer day.ogg"); } } else if (locationName == "uni_interior") { applyUniIntHud(); if (gameState_.isDarklands) { - audioPlayer_.playMusicAsync("audio/bishkek fight calm.ogg"); + audioPlayer_.crossFadeMusicAsync("audio/bishkek fight calm.ogg"); } else if (gameState_.isNight) { - audioPlayer_.playMusicAsync("audio/bishkek fight calm.ogg"); + audioPlayer_.crossFadeMusicAsync("audio/bishkek night.ogg"); } else { - audioPlayer_.playMusicAsync("audio/bishkek univer day.ogg"); + audioPlayer_.crossFadeMusicAsync("audio/bishkek univer day.ogg"); } } else { // Returning to dorm: reuse step5ab, suppress already-completed hints @@ -938,11 +1003,11 @@ namespace ZL { if (gameState_.isNight) { - audioPlayer_.playMusicAsync("audio/bishkek fight calm.ogg"); + audioPlayer_.crossFadeMusicAsync("audio/bishkek night.ogg"); } else { - audioPlayer_.playMusicAsync("audio/obshaga.ogg"); + audioPlayer_.crossFadeMusicAsync("audio/obshaga.ogg"); } } } @@ -1452,4 +1517,37 @@ namespace ZL { updateToasts(deltaMs); } + void MenuManager::saveSettings() { + nlohmann::json root; + root["musicVolume"] = audioPlayer_.getMusicVolume(); + root["soundVolume"] = audioPlayer_.getSoundVolume(); + root["musicEnabled"] = audioPlayer_.isMusicEnabled(); + root["soundEnabled"] = audioPlayer_.isSoundEnabled(); + + std::ofstream file("settings.json"); + if (file.is_open()) { + file << root.dump(2); + } else { + std::cerr << "[settings] Could not open settings.json for writing" << std::endl; + } + } + + void MenuManager::loadSettings() { + const std::string content = ZL::readTextFile("settings.json"); + if (content.empty()) return; + try { + const nlohmann::json root = nlohmann::json::parse(content); + if (root.contains("musicVolume")) + audioPlayer_.setMusicVolume(root["musicVolume"].get()); + if (root.contains("soundVolume")) + audioPlayer_.setSoundVolume(root["soundVolume"].get()); + if (root.contains("musicEnabled")) + audioPlayer_.setMusicEnabled(root["musicEnabled"].get()); + if (root.contains("soundEnabled")) + audioPlayer_.setSoundEnabled(root["soundEnabled"].get()); + } catch (const std::exception& e) { + std::cerr << "[settings] Failed to parse settings.json: " << e.what() << std::endl; + } + } + } // namespace ZL diff --git a/src/MenuManager.h b/src/MenuManager.h index e2ccedf..6f2f605 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -93,6 +93,9 @@ namespace ZL { void onCutsceneFinished(); bool cutsceneHudActive_ = false; + void saveSettings(); + void loadSettings(); + // Toast notification system void showToast(const std::string& iconPath, const std::string& text); void update(float deltaMs); diff --git a/src/UiManager.cpp b/src/UiManager.cpp index f976b54..699950e 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -327,16 +327,16 @@ namespace ZL { knobMesh.data.PositionData.clear(); knobMesh.data.TexCoordData.clear(); - float kw = vertical ? rect.w * 4.0f : rect.w * 0.5f; - float kh = vertical ? rect.w * 4.0f : rect.h * 0.5f; - - float cx = rect.x + rect.w * 0.5f; - float cy = rect.y + (vertical ? (value * rect.h) : (rect.h * 0.5f)); - - float x0 = cx - kw * 0.5f; - float y0 = cy - kh * 0.5f; - float x1 = cx + kw * 0.5f; - float y1 = cy + kh * 0.5f; + // Knob is built centered at the middle of the track rect. + // draw() applies a per-frame translation so the knob tracks the current value + // without needing to rebuild the VBO on every value change. + const float kSize = vertical ? rect.w * 2.0f : rect.h; + const float cx = rect.x + rect.w * 0.5f; + const float cy = rect.y + rect.h * 0.5f; + const float x0 = cx - kSize * 0.5f; + const float y0 = cy - kSize * 0.5f; + const float x1 = cx + kSize * 0.5f; + const float y1 = cy + kSize * 0.5f; knobMesh.data.PositionData.push_back({ x0, y0, 0 }); knobMesh.data.TexCoordData.push_back({ 0, 0 }); @@ -366,8 +366,15 @@ namespace ZL { renderer.DrawVertexRenderStruct(trackMesh); } if (texKnob) { + // The knob mesh is built centered at the middle of the track rect. + // Translate it so its center lands on the value position along the track. + const float offsetX = vertical ? 0.0f : (value - 0.5f) * rect.w; + const float offsetY = vertical ? (value - 0.5f) * rect.h : 0.0f; + renderer.PushMatrix(); + renderer.TranslateMatrix({ offsetX, offsetY, 0.0f }); glBindTexture(GL_TEXTURE_2D, texKnob->getTexID()); renderer.DrawVertexRenderStruct(knobMesh); + renderer.PopMatrix(); } } @@ -1301,7 +1308,6 @@ namespace ZL { if (fabs(s->value - value) < 1e-6f) return true; s->value = value; s->buildTrackMesh(); - s->buildKnobMesh(); if (s->onValueChanged) s->onValueChanged(s->name, s->value); return true; } @@ -1796,7 +1802,6 @@ namespace ZL { if (t > 1.0f) t = 1.0f; s->value = t; s->buildTrackMesh(); - s->buildKnobMesh(); if (s->onValueChanged) s->onValueChanged(s->name, s->value); } } @@ -1843,7 +1848,6 @@ namespace ZL { if (t > 1.0f) t = 1.0f; s->value = t; s->buildTrackMesh(); - s->buildKnobMesh(); if (s->onValueChanged) s->onValueChanged(s->name, s->value); break; }