Working on music cross fade and settings
This commit is contained in:
parent
84f9fa37a8
commit
b8d79c7b7b
BIN
audio/bishkek night.ogg
(Stored with Git LFS)
Normal file
BIN
audio/bishkek night.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
#include "AudioPlayer.hpp"
|
|
||||||
#include <iostream>
|
|
||||||
#include <thread>
|
|
||||||
#include <chrono>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <AL/al.h>
|
|
||||||
#include <AL/alc.h>
|
|
||||||
#include <vorbis/vorbisfile.h>
|
|
||||||
#include <vector>
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
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<char> loadOgg(const std::string& filename, ALuint buffer);
|
|
||||||
std::string findFileInSounds(const std::string& filename);
|
|
||||||
bool isOggFile(const std::string& filename) const;
|
|
||||||
};
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
#include "AudioPlayer.hpp"
|
|
||||||
#include <filesystem>
|
|
||||||
#include <fstream>
|
|
||||||
#include <iostream>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
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<std::filesystem::path> 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<char> 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<char> 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;
|
|
||||||
}
|
|
||||||
407
resources/dialogue/uni_exterior_dialogues.json
Normal file
407
resources/dialogue/uni_exterior_dialogues.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -11,6 +11,98 @@
|
|||||||
"height": "match_parent",
|
"height": "match_parent",
|
||||||
"texture": "resources/w/ui/img/main/bkg.png"
|
"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",
|
"type": "TextButton",
|
||||||
"name": "settingsBackButton",
|
"name": "settingsBackButton",
|
||||||
|
|||||||
@ -43,6 +43,12 @@ bool AudioPlayerAsync::init() {
|
|||||||
void AudioPlayerAsync::shutdown() {
|
void AudioPlayerAsync::shutdown() {
|
||||||
if (!initialized) return;
|
if (!initialized) return;
|
||||||
|
|
||||||
|
if (currentMusic_) {
|
||||||
|
Mix_HaltMusic();
|
||||||
|
Mix_FreeMusic(currentMusic_);
|
||||||
|
currentMusic_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(soundCacheMutex);
|
std::lock_guard<std::mutex> lock(soundCacheMutex);
|
||||||
for (auto& pair : soundCache) {
|
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;
|
std::cerr << "AudioPlayerAsync not initialized" << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!soundEnabled_) return;
|
||||||
|
|
||||||
auto task = [this, filePath, loops, channel]() {
|
auto task = [this, filePath, loops, channel]() {
|
||||||
Mix_Chunk* sound = nullptr;
|
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) {
|
void AudioPlayerAsync::playMusicAsync(const std::string& filePath, int loops) {
|
||||||
std::cout << "AudioPlayerAsync::playMusicAsync called step 1" << std::endl;
|
|
||||||
if (!initialized) return;
|
if (!initialized) return;
|
||||||
std::cout << "AudioPlayerAsync::playMusicAsync called step 1" << std::endl;
|
|
||||||
|
|
||||||
if (filePath == currentTrack)
|
if (filePath == currentTrack)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
currentTrack = filePath;
|
currentTrack = filePath;
|
||||||
|
if (!musicEnabled_) return;
|
||||||
|
|
||||||
auto task = [filePath, loops]() {
|
auto task = [this, filePath, loops]() {
|
||||||
std::cout << "AudioPlayerAsync::playMusicAsync called step 3" << std::endl;
|
Mix_HaltMusic();
|
||||||
|
if (currentMusic_) {
|
||||||
|
Mix_FreeMusic(currentMusic_);
|
||||||
|
currentMusic_ = nullptr;
|
||||||
|
}
|
||||||
Mix_Music* music = Mix_LoadMUS(filePath.c_str());
|
Mix_Music* music = Mix_LoadMUS(filePath.c_str());
|
||||||
if (!music) {
|
if (!music) {
|
||||||
std::cerr << "Failed to load music " << filePath << ": " << Mix_GetError() << std::endl;
|
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) {
|
if (Mix_PlayMusic(music, loops) == -1) {
|
||||||
std::cerr << "Mix_PlayMusic failed: " << Mix_GetError() << std::endl;
|
std::cerr << "Mix_PlayMusic failed: " << Mix_GetError() << std::endl;
|
||||||
Mix_FreeMusic(music);
|
Mix_FreeMusic(music);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
currentMusic_ = music;
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
@ -178,6 +187,7 @@ void AudioPlayerAsync::resumeMusicAsync() {
|
|||||||
void AudioPlayerAsync::setMusicVolume(int volume) {
|
void AudioPlayerAsync::setMusicVolume(int volume) {
|
||||||
if (!initialized) return;
|
if (!initialized) return;
|
||||||
volume = std::max(0, std::min(128, volume));
|
volume = std::max(0, std::min(128, volume));
|
||||||
|
musicVolume_ = volume;
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
Mix_VolumeMusic(volume);
|
Mix_VolumeMusic(volume);
|
||||||
#else
|
#else
|
||||||
@ -192,6 +202,7 @@ void AudioPlayerAsync::setMusicVolume(int volume) {
|
|||||||
void AudioPlayerAsync::setSoundVolume(int volume) {
|
void AudioPlayerAsync::setSoundVolume(int volume) {
|
||||||
if (!initialized) return;
|
if (!initialized) return;
|
||||||
volume = std::max(0, std::min(128, volume));
|
volume = std::max(0, std::min(128, volume));
|
||||||
|
soundVolume_ = volume;
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
Mix_Volume(-1, volume);
|
Mix_Volume(-1, volume);
|
||||||
#else
|
#else
|
||||||
@ -203,6 +214,117 @@ void AudioPlayerAsync::setSoundVolume(int volume) {
|
|||||||
#endif
|
#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<std::mutex> 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<std::mutex> 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<std::mutex> 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__
|
#ifndef __EMSCRIPTEN__
|
||||||
void AudioPlayerAsync::workerThread() {
|
void AudioPlayerAsync::workerThread() {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@ -24,11 +24,20 @@ public:
|
|||||||
|
|
||||||
void playSoundAsync(const std::string& filePath, int loops = 0, int channel = -1);
|
void playSoundAsync(const std::string& filePath, int loops = 0, int channel = -1);
|
||||||
void playMusicAsync(const std::string& filePath, int loops = -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 stopMusicAsync();
|
||||||
void pauseMusicAsync();
|
void pauseMusicAsync();
|
||||||
void resumeMusicAsync();
|
void resumeMusicAsync();
|
||||||
void setMusicVolume(int volume); // 0..128
|
void setMusicVolume(int volume); // 0..128
|
||||||
void setSoundVolume(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; }
|
void exit() { stop = true; }
|
||||||
|
|
||||||
@ -47,6 +56,12 @@ private:
|
|||||||
std::mutex soundCacheMutex;
|
std::mutex soundCacheMutex;
|
||||||
|
|
||||||
std::string currentTrack;
|
std::string currentTrack;
|
||||||
|
Mix_Music* currentMusic_ = nullptr;
|
||||||
|
|
||||||
|
int musicVolume_ = 128;
|
||||||
|
int soundVolume_ = 128;
|
||||||
|
bool musicEnabled_ = true;
|
||||||
|
bool soundEnabled_ = true;
|
||||||
|
|
||||||
bool initialized = false;
|
bool initialized = false;
|
||||||
};
|
};
|
||||||
10
src/Game.cpp
10
src/Game.cpp
@ -327,7 +327,7 @@ namespace ZL
|
|||||||
menuManager.startGameFunc = [this]() {
|
menuManager.startGameFunc = [this]() {
|
||||||
gameState.currentLocationName = "location_dorm";
|
gameState.currentLocationName = "location_dorm";
|
||||||
currentLocation()->scriptEngine.callLocationEnterCallback();
|
currentLocation()->scriptEngine.callLocationEnterCallback();
|
||||||
this->audioPlayer->playMusicAsync("audio/obshaga.ogg");
|
this->audioPlayer->crossFadeMusicAsync("audio/obshaga.ogg");
|
||||||
};
|
};
|
||||||
|
|
||||||
menuManager.onSaveGame = [this](int slot) { saveGame(slot); };
|
menuManager.onSaveGame = [this](int slot) { saveGame(slot); };
|
||||||
@ -343,7 +343,9 @@ namespace ZL
|
|||||||
std::cout << "Audio initialization failed" << std::endl;
|
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;
|
loadingCompleted = true;
|
||||||
|
|
||||||
@ -453,10 +455,10 @@ namespace ZL
|
|||||||
}
|
}
|
||||||
|
|
||||||
gameState.locations["uni_interior"]->requestDarklandsPlayBattleMusic = [this]() {
|
gameState.locations["uni_interior"]->requestDarklandsPlayBattleMusic = [this]() {
|
||||||
audioPlayer->playMusicAsync("audio/bishkek fight.ogg");
|
audioPlayer->crossFadeMusicFromPositionAsync("audio/bishkek fight.ogg");
|
||||||
};
|
};
|
||||||
gameState.locations["uni_interior"]->requestDarklandsPlayNormalMusic = [this]() {
|
gameState.locations["uni_interior"]->requestDarklandsPlayNormalMusic = [this]() {
|
||||||
audioPlayer->playMusicAsync("audio/bishkek fight calm.ogg");
|
audioPlayer->crossFadeMusicFromPositionAsync("audio/bishkek fight calm.ogg");
|
||||||
};
|
};
|
||||||
LocationSetup uniExteriorParams = uniInteriorParams;
|
LocationSetup uniExteriorParams = uniInteriorParams;
|
||||||
uniExteriorParams.gameObjectsJsonPath = "resources/config2/gameobjects_uni_exterior_x.json";
|
uniExteriorParams.gameObjectsJsonPath = "resources/config2/gameobjects_uni_exterior_x.json";
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
#include "MenuManager.h"
|
#include "MenuManager.h"
|
||||||
#include "Game.h"
|
#include "Game.h"
|
||||||
#include "render/TextRenderer.h"
|
#include "render/TextRenderer.h"
|
||||||
|
#include "utils/Utils.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@ -252,7 +254,7 @@ namespace ZL {
|
|||||||
void MenuManager::showMainMenu() {
|
void MenuManager::showMainMenu() {
|
||||||
if (onResetGame) onResetGame();
|
if (onResetGame) onResetGame();
|
||||||
|
|
||||||
audioPlayer_.playMusicAsync("audio/main menu final.ogg");
|
audioPlayer_.crossFadeMusicAsync("audio/main menu final.ogg");
|
||||||
|
|
||||||
uiState_ = GameUiState::MainMenu;
|
uiState_ = GameUiState::MainMenu;
|
||||||
uiManager.clearMenuStack();
|
uiManager.clearMenuStack();
|
||||||
@ -294,9 +296,72 @@ namespace ZL {
|
|||||||
void MenuManager::showSettingsScreen() {
|
void MenuManager::showSettingsScreen() {
|
||||||
uiManager.pushMenuFromSavedRoot(settingsScreenRoot);
|
uiManager.pushMenuFromSavedRoot(settingsScreenRoot);
|
||||||
uiManager.setTextButtonCallback("settingsBackButton", [this](const std::string&) {
|
uiManager.setTextButtonCallback("settingsBackButton", [this](const std::string&) {
|
||||||
|
saveSettings();
|
||||||
uiManager.popMenu();
|
uiManager.popMenu();
|
||||||
uiManager.updateAllLayouts();
|
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<int>(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<int>(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<float, 4>{ 0.2f, 0.9f, 0.2f, 1.0f }
|
||||||
|
: std::array<float, 4>{ 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<float, 4>{ 0.2f, 0.9f, 0.2f, 1.0f }
|
||||||
|
: std::array<float, 4>{ 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<float, 4>{ 0.2f, 0.9f, 0.2f, 1.0f }
|
||||||
|
: std::array<float, 4>{ 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<float, 4>{ 0.2f, 0.9f, 0.2f, 1.0f }
|
||||||
|
: std::array<float, 4>{ 0.9f, 0.2f, 0.2f, 1.0f });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::showLoadGameScreen()
|
void MenuManager::showLoadGameScreen()
|
||||||
@ -906,29 +971,29 @@ namespace ZL {
|
|||||||
setupGameplayHudCallbacks();
|
setupGameplayHudCallbacks();
|
||||||
if (gameState_.isDarklands)
|
if (gameState_.isDarklands)
|
||||||
{
|
{
|
||||||
audioPlayer_.playMusicAsync("audio/bishkek fight calm.ogg");
|
audioPlayer_.crossFadeMusicAsync("audio/bishkek fight calm.ogg");
|
||||||
}
|
}
|
||||||
else if (gameState_.isNight)
|
else if (gameState_.isNight)
|
||||||
{
|
{
|
||||||
audioPlayer_.playMusicAsync("audio/bishkek fight calm.ogg");
|
audioPlayer_.crossFadeMusicAsync("audio/bishkek night.ogg");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
audioPlayer_.playMusicAsync("audio/bishkek univer day.ogg");
|
audioPlayer_.crossFadeMusicAsync("audio/bishkek univer day.ogg");
|
||||||
}
|
}
|
||||||
} else if (locationName == "uni_interior") {
|
} else if (locationName == "uni_interior") {
|
||||||
applyUniIntHud();
|
applyUniIntHud();
|
||||||
if (gameState_.isDarklands)
|
if (gameState_.isDarklands)
|
||||||
{
|
{
|
||||||
audioPlayer_.playMusicAsync("audio/bishkek fight calm.ogg");
|
audioPlayer_.crossFadeMusicAsync("audio/bishkek fight calm.ogg");
|
||||||
}
|
}
|
||||||
else if (gameState_.isNight)
|
else if (gameState_.isNight)
|
||||||
{
|
{
|
||||||
audioPlayer_.playMusicAsync("audio/bishkek fight calm.ogg");
|
audioPlayer_.crossFadeMusicAsync("audio/bishkek night.ogg");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
audioPlayer_.playMusicAsync("audio/bishkek univer day.ogg");
|
audioPlayer_.crossFadeMusicAsync("audio/bishkek univer day.ogg");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Returning to dorm: reuse step5ab, suppress already-completed hints
|
// Returning to dorm: reuse step5ab, suppress already-completed hints
|
||||||
@ -938,11 +1003,11 @@ namespace ZL {
|
|||||||
|
|
||||||
if (gameState_.isNight)
|
if (gameState_.isNight)
|
||||||
{
|
{
|
||||||
audioPlayer_.playMusicAsync("audio/bishkek fight calm.ogg");
|
audioPlayer_.crossFadeMusicAsync("audio/bishkek night.ogg");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
audioPlayer_.playMusicAsync("audio/obshaga.ogg");
|
audioPlayer_.crossFadeMusicAsync("audio/obshaga.ogg");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1452,4 +1517,37 @@ namespace ZL {
|
|||||||
updateToasts(deltaMs);
|
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<int>());
|
||||||
|
if (root.contains("soundVolume"))
|
||||||
|
audioPlayer_.setSoundVolume(root["soundVolume"].get<int>());
|
||||||
|
if (root.contains("musicEnabled"))
|
||||||
|
audioPlayer_.setMusicEnabled(root["musicEnabled"].get<bool>());
|
||||||
|
if (root.contains("soundEnabled"))
|
||||||
|
audioPlayer_.setSoundEnabled(root["soundEnabled"].get<bool>());
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[settings] Failed to parse settings.json: " << e.what() << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ZL
|
} // namespace ZL
|
||||||
|
|||||||
@ -93,6 +93,9 @@ namespace ZL {
|
|||||||
void onCutsceneFinished();
|
void onCutsceneFinished();
|
||||||
bool cutsceneHudActive_ = false;
|
bool cutsceneHudActive_ = false;
|
||||||
|
|
||||||
|
void saveSettings();
|
||||||
|
void loadSettings();
|
||||||
|
|
||||||
// Toast notification system
|
// Toast notification system
|
||||||
void showToast(const std::string& iconPath, const std::string& text);
|
void showToast(const std::string& iconPath, const std::string& text);
|
||||||
void update(float deltaMs);
|
void update(float deltaMs);
|
||||||
|
|||||||
@ -327,16 +327,16 @@ namespace ZL {
|
|||||||
knobMesh.data.PositionData.clear();
|
knobMesh.data.PositionData.clear();
|
||||||
knobMesh.data.TexCoordData.clear();
|
knobMesh.data.TexCoordData.clear();
|
||||||
|
|
||||||
float kw = vertical ? rect.w * 4.0f : rect.w * 0.5f;
|
// Knob is built centered at the middle of the track rect.
|
||||||
float kh = vertical ? rect.w * 4.0f : rect.h * 0.5f;
|
// draw() applies a per-frame translation so the knob tracks the current value
|
||||||
|
// without needing to rebuild the VBO on every value change.
|
||||||
float cx = rect.x + rect.w * 0.5f;
|
const float kSize = vertical ? rect.w * 2.0f : rect.h;
|
||||||
float cy = rect.y + (vertical ? (value * rect.h) : (rect.h * 0.5f));
|
const float cx = rect.x + rect.w * 0.5f;
|
||||||
|
const float cy = rect.y + rect.h * 0.5f;
|
||||||
float x0 = cx - kw * 0.5f;
|
const float x0 = cx - kSize * 0.5f;
|
||||||
float y0 = cy - kh * 0.5f;
|
const float y0 = cy - kSize * 0.5f;
|
||||||
float x1 = cx + kw * 0.5f;
|
const float x1 = cx + kSize * 0.5f;
|
||||||
float y1 = cy + kh * 0.5f;
|
const float y1 = cy + kSize * 0.5f;
|
||||||
|
|
||||||
knobMesh.data.PositionData.push_back({ x0, y0, 0 });
|
knobMesh.data.PositionData.push_back({ x0, y0, 0 });
|
||||||
knobMesh.data.TexCoordData.push_back({ 0, 0 });
|
knobMesh.data.TexCoordData.push_back({ 0, 0 });
|
||||||
@ -366,8 +366,15 @@ namespace ZL {
|
|||||||
renderer.DrawVertexRenderStruct(trackMesh);
|
renderer.DrawVertexRenderStruct(trackMesh);
|
||||||
}
|
}
|
||||||
if (texKnob) {
|
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());
|
glBindTexture(GL_TEXTURE_2D, texKnob->getTexID());
|
||||||
renderer.DrawVertexRenderStruct(knobMesh);
|
renderer.DrawVertexRenderStruct(knobMesh);
|
||||||
|
renderer.PopMatrix();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1301,7 +1308,6 @@ namespace ZL {
|
|||||||
if (fabs(s->value - value) < 1e-6f) return true;
|
if (fabs(s->value - value) < 1e-6f) return true;
|
||||||
s->value = value;
|
s->value = value;
|
||||||
s->buildTrackMesh();
|
s->buildTrackMesh();
|
||||||
s->buildKnobMesh();
|
|
||||||
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
|
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -1796,7 +1802,6 @@ namespace ZL {
|
|||||||
if (t > 1.0f) t = 1.0f;
|
if (t > 1.0f) t = 1.0f;
|
||||||
s->value = t;
|
s->value = t;
|
||||||
s->buildTrackMesh();
|
s->buildTrackMesh();
|
||||||
s->buildKnobMesh();
|
|
||||||
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
|
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1843,7 +1848,6 @@ namespace ZL {
|
|||||||
if (t > 1.0f) t = 1.0f;
|
if (t > 1.0f) t = 1.0f;
|
||||||
s->value = t;
|
s->value = t;
|
||||||
s->buildTrackMesh();
|
s->buildTrackMesh();
|
||||||
s->buildKnobMesh();
|
|
||||||
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
|
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user