From 9f0dfc4e24d765647392970da1279297f18631c5 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 3 Dec 2025 09:42:45 +0100 Subject: [PATCH] =?UTF-8?q?gitignore=20no=20ha=20deixat=20versionar=20cap?= =?UTF-8?q?=20fitxer=20de=20core=20afegida=20gesti=C3=B3=20de=20ratol?= =?UTF-8?q?=C3=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +- source/core/audio/audio.cpp | 183 +++++++ source/core/audio/audio.hpp | 97 ++++ source/core/audio/audio_cache.cpp | 118 +++++ source/core/audio/audio_cache.hpp | 42 ++ source/core/audio/jail_audio.hpp | 482 ++++++++++++++++++ source/core/defaults.hpp | 167 ++++++ source/core/graphics/shape.cpp | 155 ++++++ source/core/graphics/shape.hpp | 64 +++ source/core/graphics/shape_loader.cpp | 72 +++ source/core/graphics/shape_loader.hpp | 39 ++ source/core/graphics/starfield.cpp | 163 ++++++ source/core/graphics/starfield.hpp | 80 +++ source/core/graphics/vector_text.cpp | 263 ++++++++++ source/core/graphics/vector_text.hpp | 46 ++ source/core/input/mouse.cpp | 63 +++ source/core/input/mouse.hpp | 16 + source/core/rendering/color_oscillator.cpp | 68 +++ source/core/rendering/color_oscillator.hpp | 29 ++ .../core/rendering/coordinate_transform.cpp | 11 + .../core/rendering/coordinate_transform.hpp | 31 ++ source/core/rendering/line_renderer.cpp | 101 ++++ source/core/rendering/line_renderer.hpp | 16 + source/core/rendering/polygon_renderer.cpp | 86 ++++ source/core/rendering/polygon_renderer.hpp | 22 + source/core/rendering/primitives.cpp | 66 +++ source/core/rendering/primitives.hpp | 32 ++ source/core/rendering/sdl_manager.cpp | 8 + source/core/rendering/shape_renderer.cpp | 80 +++ source/core/rendering/shape_renderer.hpp | 33 ++ source/core/system/director.cpp | 191 +++++++ source/core/system/director.hpp | 20 + source/core/system/gestor_escenes.hpp | 17 + source/core/system/global_events.cpp | 48 ++ source/core/system/global_events.hpp | 16 + source/core/types.hpp | 43 ++ source/game/escenes/escena_joc.cpp | 4 + source/game/escenes/escena_logo.cpp | 4 + source/game/escenes/escena_titol.cpp | 4 + 39 files changed, 2983 insertions(+), 3 deletions(-) create mode 100644 source/core/audio/audio.cpp create mode 100644 source/core/audio/audio.hpp create mode 100644 source/core/audio/audio_cache.cpp create mode 100644 source/core/audio/audio_cache.hpp create mode 100644 source/core/audio/jail_audio.hpp create mode 100644 source/core/defaults.hpp create mode 100644 source/core/graphics/shape.cpp create mode 100644 source/core/graphics/shape.hpp create mode 100644 source/core/graphics/shape_loader.cpp create mode 100644 source/core/graphics/shape_loader.hpp create mode 100644 source/core/graphics/starfield.cpp create mode 100644 source/core/graphics/starfield.hpp create mode 100644 source/core/graphics/vector_text.cpp create mode 100644 source/core/graphics/vector_text.hpp create mode 100644 source/core/input/mouse.cpp create mode 100644 source/core/input/mouse.hpp create mode 100644 source/core/rendering/color_oscillator.cpp create mode 100644 source/core/rendering/color_oscillator.hpp create mode 100644 source/core/rendering/coordinate_transform.cpp create mode 100644 source/core/rendering/coordinate_transform.hpp create mode 100644 source/core/rendering/line_renderer.cpp create mode 100644 source/core/rendering/line_renderer.hpp create mode 100644 source/core/rendering/polygon_renderer.cpp create mode 100644 source/core/rendering/polygon_renderer.hpp create mode 100644 source/core/rendering/primitives.cpp create mode 100644 source/core/rendering/primitives.hpp create mode 100644 source/core/rendering/shape_renderer.cpp create mode 100644 source/core/rendering/shape_renderer.hpp create mode 100644 source/core/system/director.cpp create mode 100644 source/core/system/director.hpp create mode 100644 source/core/system/gestor_escenes.hpp create mode 100644 source/core/system/global_events.cpp create mode 100644 source/core/system/global_events.hpp create mode 100644 source/core/types.hpp diff --git a/.gitignore b/.gitignore index f61a98c..527acec 100644 --- a/.gitignore +++ b/.gitignore @@ -58,9 +58,9 @@ _deps/ *.ilk # Core dumps -core -core.* -*.core +# core +# core.* +# *.core # macOS .DS_Store diff --git a/source/core/audio/audio.cpp b/source/core/audio/audio.cpp new file mode 100644 index 0000000..be1eac7 --- /dev/null +++ b/source/core/audio/audio.cpp @@ -0,0 +1,183 @@ +#include "audio.hpp" + +#include // Para SDL_LogInfo, SDL_LogCategory, SDL_G... + +#include // Para clamp +#include // Para std::cout + +// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp) +// clang-format off +#undef STB_VORBIS_HEADER_ONLY +#include "external/stb_vorbis.h" +// clang-format on + +#include "core/audio/audio_cache.hpp" // Para AudioCache +#include "core/audio/jail_audio.hpp" // Para JA_FadeOutMusic, JA_Init, JA_PauseM... +#include "game/options.hpp" // Para AudioOptions, audio, MusicOptions + +// Singleton +Audio* Audio::instance = nullptr; + +// Inicializa la instancia única del singleton +void Audio::init() { Audio::instance = new Audio(); } + +// Libera la instancia +void Audio::destroy() { delete Audio::instance; } + +// Obtiene la instancia +auto Audio::get() -> Audio* { return Audio::instance; } + +// Constructor +Audio::Audio() { initSDLAudio(); } + +// Destructor +Audio::~Audio() { + JA_Quit(); +} + +// Método principal +void Audio::update() { + JA_Update(); +} + +// Reproduce la música +void Audio::playMusic(const std::string& name, const int loop) { + bool new_loop = (loop != 0); + + // Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada + if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) { + return; + } + + // Intentar obtener recurso; si falla, no tocar estado + auto* resource = AudioCache::getMusic(name); + if (resource == nullptr) { + // manejo de error opcional + return; + } + + // Si hay algo reproduciéndose, detenerlo primero (si el backend lo requiere) + if (music_.state == MusicState::PLAYING) { + JA_StopMusic(); // sustituir por la función de stop real del API si tiene otro nombre + } + + // Llamada al motor para reproducir la nueva pista + JA_PlayMusic(resource, loop); + + // Actualizar estado y metadatos después de iniciar con éxito + music_.name = name; + music_.loop = new_loop; + music_.state = MusicState::PLAYING; +} + +// Pausa la música +void Audio::pauseMusic() { + if (music_enabled_ && music_.state == MusicState::PLAYING) { + JA_PauseMusic(); + music_.state = MusicState::PAUSED; + } +} + +// Continua la música pausada +void Audio::resumeMusic() { + if (music_enabled_ && music_.state == MusicState::PAUSED) { + JA_ResumeMusic(); + music_.state = MusicState::PLAYING; + } +} + +// Detiene la música +void Audio::stopMusic() { + if (music_enabled_) { + JA_StopMusic(); + music_.state = MusicState::STOPPED; + } +} + +// Reproduce un sonido por nombre +void Audio::playSound(const std::string& name, Group group) const { + if (sound_enabled_) { + JA_PlaySound(AudioCache::getSound(name), 0, static_cast(group)); + } +} + +// Reproduce un sonido por puntero directo +void Audio::playSound(JA_Sound_t* sound, Group group) const { + if (sound_enabled_) { + JA_PlaySound(sound, 0, static_cast(group)); + } +} + +// Detiene todos los sonidos +void Audio::stopAllSounds() const { + if (sound_enabled_) { + JA_StopChannel(-1); + } +} + +// Realiza un fundido de salida de la música +void Audio::fadeOutMusic(int milliseconds) const { + if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) { + JA_FadeOutMusic(milliseconds); + } +} + +// Consulta directamente el estado real de la música en jailaudio +auto Audio::getRealMusicState() -> MusicState { + JA_Music_state ja_state = JA_GetMusicState(); + switch (ja_state) { + case JA_MUSIC_PLAYING: + return MusicState::PLAYING; + case JA_MUSIC_PAUSED: + return MusicState::PAUSED; + case JA_MUSIC_STOPPED: + case JA_MUSIC_INVALID: + case JA_MUSIC_DISABLED: + default: + return MusicState::STOPPED; + } +} + +// Establece el volumen de los sonidos +void Audio::setSoundVolume(float sound_volume, Group group) const { + if (sound_enabled_) { + sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME); + const float CONVERTED_VOLUME = sound_volume * Options::audio.volume; + JA_SetSoundVolume(CONVERTED_VOLUME, static_cast(group)); + } +} + +// Establece el volumen de la música +void Audio::setMusicVolume(float music_volume) const { + if (music_enabled_) { + music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME); + const float CONVERTED_VOLUME = music_volume * Options::audio.volume; + JA_SetMusicVolume(CONVERTED_VOLUME); + } +} + +// Aplica la configuración +void Audio::applySettings() { + enable(Options::audio.enabled); +} + +// Establecer estado general +void Audio::enable(bool value) { + enabled_ = value; + + setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME); + setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME); +} + +// Inicializa SDL Audio +void Audio::initSDLAudio() { + if (!SDL_Init(SDL_INIT_AUDIO)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AUDIO could not initialize! SDL Error: %s", SDL_GetError()); + } else { + JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2); + enable(Options::audio.enabled); + + std::cout << "\n** AUDIO SYSTEM **\n"; + std::cout << "Audio system initialized successfully\n"; + } +} \ No newline at end of file diff --git a/source/core/audio/audio.hpp b/source/core/audio/audio.hpp new file mode 100644 index 0000000..16ff733 --- /dev/null +++ b/source/core/audio/audio.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include // Para string +#include // Para move + +// --- Clase Audio: gestor de audio (singleton) --- +class Audio { + public: + // --- Enums --- + enum class Group : int { + ALL = -1, // Todos los grupos + GAME = 0, // Sonidos del juego + INTERFACE = 1 // Sonidos de la interfaz + }; + + enum class MusicState { + PLAYING, // Reproduciendo música + PAUSED, // Música pausada + STOPPED, // Música detenida + }; + + // --- Constantes --- + static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo + static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo + static constexpr int FREQUENCY = 48000; // Frecuencia de audio + + // --- Singleton --- + static void init(); // Inicializa el objeto Audio + static void destroy(); // Libera el objeto Audio + static auto get() -> Audio*; // Obtiene el puntero al objeto Audio + Audio(const Audio&) = delete; // Evitar copia + auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación + + static void update(); // Actualización del sistema de audio + + // --- Control de música --- + void playMusic(const std::string& name, int loop = -1); // Reproducir música en bucle + void pauseMusic(); // Pausar reproducción de música + void resumeMusic(); // Continua la música pausada + void stopMusic(); // Detener completamente la música + void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música + + // --- Control de sonidos --- + void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre + void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero + void stopAllSounds() const; // Detener todos los sonidos + + // --- Control de volumen --- + void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos + void setMusicVolume(float volume) const; // Ajustar volumen de música + + // --- Configuración general --- + void enable(bool value); // Establecer estado general + void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general + void applySettings(); // Aplica la configuración + + // --- Configuración de sonidos --- + void enableSound() { sound_enabled_ = true; } // Habilitar sonidos + void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos + void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos + void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos + + // --- Configuración de música --- + void enableMusic() { music_enabled_ = true; } // Habilitar música + void disableMusic() { music_enabled_ = false; } // Deshabilitar música + void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música + void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música + + // --- Consultas de estado --- + [[nodiscard]] auto isEnabled() const -> bool { return enabled_; } + [[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; } + [[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; } + [[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; } + [[nodiscard]] static auto getRealMusicState() -> MusicState; + [[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; } + + private: + // --- Tipos anidados --- + struct Music { + MusicState state{MusicState::STOPPED}; // Estado actual de la música + std::string name; // Última pista de música reproducida + bool loop{false}; // Indica si se reproduce en bucle + }; + + // --- Métodos --- + Audio(); // Constructor privado + ~Audio(); // Destructor privado + void initSDLAudio(); // Inicializa SDL Audio + + // --- Variables miembro --- + static Audio* instance; // Instancia única de Audio + + Music music_; // Estado de la música + bool enabled_{true}; // Estado general del audio + bool sound_enabled_{true}; // Estado de los efectos de sonido + bool music_enabled_{true}; // Estado de la música +}; \ No newline at end of file diff --git a/source/core/audio/audio_cache.cpp b/source/core/audio/audio_cache.cpp new file mode 100644 index 0000000..f749ecd --- /dev/null +++ b/source/core/audio/audio_cache.cpp @@ -0,0 +1,118 @@ +// audio_cache.cpp - Implementació del caché de sons i música +// © 2025 Port a C++20 amb SDL3 + +#include "core/audio/audio_cache.hpp" + +#include + +// Inicialització de variables estàtiques +std::unordered_map AudioCache::sounds_; +std::unordered_map AudioCache::musics_; +std::string AudioCache::sounds_base_path_ = "data/sounds/"; +std::string AudioCache::music_base_path_ = "data/music/"; + +JA_Sound_t* AudioCache::getSound(const std::string& name) { + // Cache hit + auto it = sounds_.find(name); + if (it != sounds_.end()) { + std::cout << "[AudioCache] Sound cache hit: " << name << std::endl; + return it->second; + } + + // Cache miss - cargar archivo + std::string fullpath = resolveSoundPath(name); + JA_Sound_t* sound = JA_LoadSound(fullpath.c_str()); + + if (sound == nullptr) { + std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << fullpath + << std::endl; + return nullptr; + } + + std::cout << "[AudioCache] Sound loaded: " << name << std::endl; + sounds_[name] = sound; + return sound; +} + +JA_Music_t* AudioCache::getMusic(const std::string& name) { + // Cache hit + auto it = musics_.find(name); + if (it != musics_.end()) { + std::cout << "[AudioCache] Music cache hit: " << name << std::endl; + return it->second; + } + + // Cache miss - cargar archivo + std::string fullpath = resolveMusicPath(name); + JA_Music_t* music = JA_LoadMusic(fullpath.c_str()); + + if (music == nullptr) { + std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << fullpath + << std::endl; + return nullptr; + } + + std::cout << "[AudioCache] Music loaded: " << name << std::endl; + musics_[name] = music; + return music; +} + +void AudioCache::clear() { + std::cout << "[AudioCache] Clearing cache (" << sounds_.size() << " sounds, " + << musics_.size() << " music)" << std::endl; + + // Liberar memoria de sonidos + for (auto& [name, sound] : sounds_) { + if (sound && sound->buffer) { + SDL_free(sound->buffer); + } + delete sound; + } + sounds_.clear(); + + // Liberar memoria de música + for (auto& [name, music] : musics_) { + if (music && music->buffer) { + SDL_free(music->buffer); + } + if (music && music->filename) { + free(music->filename); + } + delete music; + } + musics_.clear(); +} + +size_t AudioCache::getSoundCacheSize() { return sounds_.size(); } + +size_t AudioCache::getMusicCacheSize() { return musics_.size(); } + +std::string AudioCache::resolveSoundPath(const std::string& name) { + // Si es un path absoluto (comienza con '/'), usarlo directamente + if (!name.empty() && name[0] == '/') { + return name; + } + + // Si ya contiene el prefix base_path, usarlo directamente + if (name.find(sounds_base_path_) == 0) { + return name; + } + + // Caso contrario, añadir base_path + return sounds_base_path_ + name; +} + +std::string AudioCache::resolveMusicPath(const std::string& name) { + // Si es un path absoluto (comienza con '/'), usarlo directamente + if (!name.empty() && name[0] == '/') { + return name; + } + + // Si ya contiene el prefix base_path, usarlo directamente + if (name.find(music_base_path_) == 0) { + return name; + } + + // Caso contrario, añadir base_path + return music_base_path_ + name; +} diff --git a/source/core/audio/audio_cache.hpp b/source/core/audio/audio_cache.hpp new file mode 100644 index 0000000..b209952 --- /dev/null +++ b/source/core/audio/audio_cache.hpp @@ -0,0 +1,42 @@ +// audio_cache.hpp - Caché simplificado de sonidos y música +// © 2025 Port a C++20 amb SDL3 + +#pragma once + +#include +#include + +#include "core/audio/jail_audio.hpp" + +// Caché estático de sonidos y música +// Patrón inspirado en Graphics::ShapeLoader +class AudioCache { + public: + // No instanciable (todo estático) + AudioCache() = delete; + + // Obtener sonido (carga bajo demanda) + // Retorna puntero (nullptr si error) + static JA_Sound_t* getSound(const std::string& name); + + // Obtener música (carga bajo demanda) + // Retorna puntero (nullptr si error) + static JA_Music_t* getMusic(const std::string& name); + + // Limpiar caché (útil para debug/recarga) + static void clear(); + + // Estadísticas (debug) + static size_t getSoundCacheSize(); + static size_t getMusicCacheSize(); + + private: + static std::unordered_map sounds_; + static std::unordered_map musics_; + static std::string sounds_base_path_; // "data/sounds/" + static std::string music_base_path_; // "data/music/" + + // Helpers privados + static std::string resolveSoundPath(const std::string& name); + static std::string resolveMusicPath(const std::string& name); +}; diff --git a/source/core/audio/jail_audio.hpp b/source/core/audio/jail_audio.hpp new file mode 100644 index 0000000..bfcb03e --- /dev/null +++ b/source/core/audio/jail_audio.hpp @@ -0,0 +1,482 @@ +#pragma once + +// --- Includes --- +#include +#include // Para uint32_t, uint8_t +#include // Para NULL, fseek, printf, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET +#include // Para free, malloc +#include // Para strcpy, strlen + +#define STB_VORBIS_HEADER_ONLY +#include "external/stb_vorbis.h" // Para stb_vorbis_decode_memory + +// --- Public Enums --- +enum JA_Channel_state { JA_CHANNEL_INVALID, + JA_CHANNEL_FREE, + JA_CHANNEL_PLAYING, + JA_CHANNEL_PAUSED, + JA_SOUND_DISABLED }; +enum JA_Music_state { JA_MUSIC_INVALID, + JA_MUSIC_PLAYING, + JA_MUSIC_PAUSED, + JA_MUSIC_STOPPED, + JA_MUSIC_DISABLED }; + +// --- Struct Definitions --- +#define JA_MAX_SIMULTANEOUS_CHANNELS 20 +#define JA_MAX_GROUPS 2 + +struct JA_Sound_t { + SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; + Uint32 length{0}; + Uint8* buffer{NULL}; +}; + +struct JA_Channel_t { + JA_Sound_t* sound{nullptr}; + int pos{0}; + int times{0}; + int group{0}; + SDL_AudioStream* stream{nullptr}; + JA_Channel_state state{JA_CHANNEL_FREE}; +}; + +struct JA_Music_t { + SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000}; + Uint32 length{0}; + Uint8* buffer{nullptr}; + char* filename{nullptr}; + + int pos{0}; + int times{0}; + SDL_AudioStream* stream{nullptr}; + JA_Music_state state{JA_MUSIC_INVALID}; +}; + +// --- Internal Global State --- +// Marcado 'inline' (C++17) para asegurar una única instancia. + +inline JA_Music_t* current_music{nullptr}; +inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS]; + +inline SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000}; +inline float JA_musicVolume{1.0f}; +inline float JA_soundVolume[JA_MAX_GROUPS]; +inline bool JA_musicEnabled{true}; +inline bool JA_soundEnabled{true}; +inline SDL_AudioDeviceID sdlAudioDevice{0}; + +inline bool fading{false}; +inline int fade_start_time{0}; +inline int fade_duration{0}; +inline float fade_initial_volume{0.0f}; // Corregido de 'int' a 'float' + +// --- Forward Declarations --- +inline void JA_StopMusic(); +inline void JA_StopChannel(const int channel); +inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0); + +// --- Core Functions --- + +inline void JA_Update() { + if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) { + if (fading) { + int time = SDL_GetTicks(); + if (time > (fade_start_time + fade_duration)) { + fading = false; + JA_StopMusic(); + return; + } else { + const int time_passed = time - fade_start_time; + const float percent = (float)time_passed / (float)fade_duration; + SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0 - percent)); + } + } + + if (current_music->times != 0) { + if ((Uint32)SDL_GetAudioStreamAvailable(current_music->stream) < (current_music->length / 2)) { + SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length); + } + if (current_music->times > 0) current_music->times--; + } else { + if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic(); + } + } + + if (JA_soundEnabled) { + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) + if (channels[i].state == JA_CHANNEL_PLAYING) { + if (channels[i].times != 0) { + if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) { + SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length); + if (channels[i].times > 0) channels[i].times--; + } + } else { + if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i); + } + } + } +} + +inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) { +#ifdef _DEBUG + SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG); +#endif + + JA_audioSpec = {format, num_channels, freq}; + if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice + sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec); + if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!"); + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE; + for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f; +} + +inline void JA_Quit() { + if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice + sdlAudioDevice = 0; +} + +// --- Music Functions --- + +inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) { + JA_Music_t* music = new JA_Music_t(); + + int chan, samplerate; + short* output; + music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2; + + music->spec.channels = chan; + music->spec.freq = samplerate; + music->spec.format = SDL_AUDIO_S16; + music->buffer = static_cast(SDL_malloc(music->length)); + SDL_memcpy(music->buffer, output, music->length); + free(output); + music->pos = 0; + music->state = JA_MUSIC_STOPPED; + + return music; +} + +inline JA_Music_t* JA_LoadMusic(const char* filename) { + // [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid. + FILE* f = fopen(filename, "rb"); + if (!f) return NULL; // Añadida comprobación de apertura + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + fseek(f, 0, SEEK_SET); + auto* buffer = static_cast(malloc(fsize + 1)); + if (!buffer) { // Añadida comprobación de malloc + fclose(f); + return NULL; + } + if (fread(buffer, fsize, 1, f) != 1) { + fclose(f); + free(buffer); + return NULL; + } + fclose(f); + + JA_Music_t* music = JA_LoadMusic(buffer, fsize); + if (music) { // Comprobar que JA_LoadMusic tuvo éxito + music->filename = static_cast(malloc(strlen(filename) + 1)); + if (music->filename) { + strcpy(music->filename, filename); + } + } + + free(buffer); + + return music; +} + +inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) { + if (!JA_musicEnabled || !music) return; // Añadida comprobación de music + + JA_StopMusic(); + + current_music = music; + current_music->pos = 0; + current_music->state = JA_MUSIC_PLAYING; + current_music->times = loop; + + current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec); + if (!current_music->stream) { // Comprobar creación de stream + SDL_Log("Failed to create audio stream!"); + current_music->state = JA_MUSIC_STOPPED; + return; + } + if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n"); + SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); + if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n"); +} + +inline char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) { + if (!music) music = current_music; + if (!music) return nullptr; // Añadida comprobación + return music->filename; +} + +inline void JA_PauseMusic() { + if (!JA_musicEnabled) return; + if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; // Comprobación mejorada + + current_music->state = JA_MUSIC_PAUSED; + SDL_UnbindAudioStream(current_music->stream); +} + +inline void JA_ResumeMusic() { + if (!JA_musicEnabled) return; + if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; // Comprobación mejorada + + current_music->state = JA_MUSIC_PLAYING; + SDL_BindAudioStream(sdlAudioDevice, current_music->stream); +} + +inline void JA_StopMusic() { + if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return; + + current_music->pos = 0; + current_music->state = JA_MUSIC_STOPPED; + if (current_music->stream) { + SDL_DestroyAudioStream(current_music->stream); + current_music->stream = nullptr; + } + // No liberamos filename aquí, se debería liberar en JA_DeleteMusic +} + +inline void JA_FadeOutMusic(const int milliseconds) { + if (!JA_musicEnabled) return; + if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return; + + fading = true; + fade_start_time = SDL_GetTicks(); + fade_duration = milliseconds; + fade_initial_volume = JA_musicVolume; +} + +inline JA_Music_state JA_GetMusicState() { + if (!JA_musicEnabled) return JA_MUSIC_DISABLED; + if (!current_music) return JA_MUSIC_INVALID; + + return current_music->state; +} + +inline void JA_DeleteMusic(JA_Music_t* music) { + if (!music) return; + if (current_music == music) { + JA_StopMusic(); + current_music = nullptr; + } + SDL_free(music->buffer); + if (music->stream) SDL_DestroyAudioStream(music->stream); + free(music->filename); // filename se libera aquí + delete music; +} + +inline float JA_SetMusicVolume(float volume) { + JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f); + if (current_music && current_music->stream) { + SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume); + } + return JA_musicVolume; +} + +inline void JA_SetMusicPosition(float value) { + if (!current_music) return; + current_music->pos = value * current_music->spec.freq; + // Nota: Esta implementación de 'pos' no parece usarse en JA_Update para + // el streaming. El streaming siempre parece empezar desde el principio. +} + +inline float JA_GetMusicPosition() { + if (!current_music) return 0; + return float(current_music->pos) / float(current_music->spec.freq); + // Nota: Ver `JA_SetMusicPosition` +} + +inline void JA_EnableMusic(const bool value) { + if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic(); + + JA_musicEnabled = value; +} + +// --- Sound Functions --- + +inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) { + JA_Sound_t* sound = new JA_Sound_t(); + sound->buffer = buffer; + sound->length = length; + // Nota: spec se queda con los valores por defecto. + return sound; +} + +inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) { + JA_Sound_t* sound = new JA_Sound_t(); + if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) { + SDL_Log("Failed to load WAV from memory: %s", SDL_GetError()); + delete sound; + return nullptr; + } + return sound; +} + +inline JA_Sound_t* JA_LoadSound(const char* filename) { + JA_Sound_t* sound = new JA_Sound_t(); + if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) { + SDL_Log("Failed to load WAV file: %s", SDL_GetError()); + delete sound; + return nullptr; + } + return sound; +} + +inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) { + if (!JA_soundEnabled || !sound) return -1; + + int channel = 0; + while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; } + if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) { + // No hay canal libre, reemplazamos el primero + channel = 0; + } + + return JA_PlaySoundOnChannel(sound, channel, loop, group); +} + +inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) { + if (!JA_soundEnabled || !sound) return -1; + if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1; + + JA_StopChannel(channel); // Detiene y limpia el canal si estaba en uso + + channels[channel].sound = sound; + channels[channel].times = loop; + channels[channel].pos = 0; + channels[channel].group = group; // Asignar grupo + channels[channel].state = JA_CHANNEL_PLAYING; + channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec); + + if (!channels[channel].stream) { + SDL_Log("Failed to create audio stream for sound!"); + channels[channel].state = JA_CHANNEL_FREE; + return -1; + } + + SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length); + SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]); + SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream); + + return channel; +} + +inline void JA_DeleteSound(JA_Sound_t* sound) { + if (!sound) return; + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { + if (channels[i].sound == sound) JA_StopChannel(i); + } + SDL_free(sound->buffer); + delete sound; +} + +inline void JA_PauseChannel(const int channel) { + if (!JA_soundEnabled) return; + + if (channel == -1) { + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) + if (channels[i].state == JA_CHANNEL_PLAYING) { + channels[i].state = JA_CHANNEL_PAUSED; + SDL_UnbindAudioStream(channels[i].stream); + } + } else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) { + if (channels[channel].state == JA_CHANNEL_PLAYING) { + channels[channel].state = JA_CHANNEL_PAUSED; + SDL_UnbindAudioStream(channels[channel].stream); + } + } +} + +inline void JA_ResumeChannel(const int channel) { + if (!JA_soundEnabled) return; + + if (channel == -1) { + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) + if (channels[i].state == JA_CHANNEL_PAUSED) { + channels[i].state = JA_CHANNEL_PLAYING; + SDL_BindAudioStream(sdlAudioDevice, channels[i].stream); + } + } else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) { + if (channels[channel].state == JA_CHANNEL_PAUSED) { + channels[channel].state = JA_CHANNEL_PLAYING; + SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream); + } + } +} + +inline void JA_StopChannel(const int channel) { + if (channel == -1) { + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { + if (channels[i].state != JA_CHANNEL_FREE) { + if (channels[i].stream) SDL_DestroyAudioStream(channels[i].stream); + channels[i].stream = nullptr; + channels[i].state = JA_CHANNEL_FREE; + channels[i].pos = 0; + channels[i].sound = NULL; + } + } + } else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) { + if (channels[channel].state != JA_CHANNEL_FREE) { + if (channels[channel].stream) SDL_DestroyAudioStream(channels[channel].stream); + channels[channel].stream = nullptr; + channels[channel].state = JA_CHANNEL_FREE; + channels[channel].pos = 0; + channels[channel].sound = NULL; + } + } +} + +inline JA_Channel_state JA_GetChannelState(const int channel) { + if (!JA_soundEnabled) return JA_SOUND_DISABLED; + if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID; + + return channels[channel].state; +} + +inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para todos los grupos +{ + const float v = SDL_clamp(volume, 0.0f, 1.0f); + + if (group == -1) { + for (int i = 0; i < JA_MAX_GROUPS; ++i) { + JA_soundVolume[i] = v; + } + } else if (group >= 0 && group < JA_MAX_GROUPS) { + JA_soundVolume[group] = v; + } else { + return v; // Grupo inválido + } + + // Aplicar volumen a canales activos + for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) { + if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) { + if (group == -1 || channels[i].group == group) { + if (channels[i].stream) { + SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]); + } + } + } + } + return v; +} + +inline void JA_EnableSound(const bool value) { + if (!value) { + JA_StopChannel(-1); // Detener todos los canales + } + JA_soundEnabled = value; +} + +inline float JA_SetVolume(float volume) { + float v = JA_SetMusicVolume(volume); + JA_SetSoundVolume(v, -1); // Aplicar a todos los grupos de sonido + return v; +} \ No newline at end of file diff --git a/source/core/defaults.hpp b/source/core/defaults.hpp new file mode 100644 index 0000000..b5e4443 --- /dev/null +++ b/source/core/defaults.hpp @@ -0,0 +1,167 @@ +#pragma once +#include + +#include + +namespace Defaults { +// Configuración de ventana +namespace Window { +constexpr int WIDTH = 640; +constexpr int HEIGHT = 480; +constexpr int MIN_WIDTH = 320; // Mínimo: mitad del original +constexpr int MIN_HEIGHT = 240; +// Zoom system +constexpr float BASE_ZOOM = 1.0f; // 640x480 baseline +constexpr float MIN_ZOOM = 0.5f; // 320x240 minimum +constexpr float ZOOM_INCREMENT = 0.1f; // 10% steps (F1/F2) +} // namespace Window + +// Dimensions base del joc (coordenades lògiques) +namespace Game { +constexpr int WIDTH = 640; +constexpr int HEIGHT = 480; +} // namespace Game + +// Zones del joc (SDL_FRect amb càlculs automàtics) +namespace Zones { +// --- CONFIGURACIÓ DE PORCENTATGES --- +// Basats en valors originals 640x480 +// Ajusta estos valors per canviar proporcions + +constexpr float PLAYAREA_MARGIN_HORIZONTAL_PERCENT = 10.0f / Game::WIDTH; +constexpr float PLAYAREA_MARGIN_VERTICAL_PERCENT = 10.0f / Game::HEIGHT; +constexpr float SCOREBOARD_HEIGHT_PERCENT = 48.0f / Game::HEIGHT; + +// --- CÀLCULS AUTOMÀTICS --- +// Estos valors es recalculen si canvien Game::WIDTH o Game::HEIGHT + +constexpr float PLAYAREA_MARGIN_H = + Game::WIDTH * PLAYAREA_MARGIN_HORIZONTAL_PERCENT; +constexpr float PLAYAREA_MARGIN_V = + Game::HEIGHT * PLAYAREA_MARGIN_VERTICAL_PERCENT; +constexpr float SCOREBOARD_H = Game::HEIGHT * SCOREBOARD_HEIGHT_PERCENT; + +// --- ZONES FINALS (SDL_FRect) --- + +// Zona de joc principal +// Ocupa: tot menys marges (dalt, esq, dret) i scoreboard (baix) +constexpr SDL_FRect PLAYAREA = { + PLAYAREA_MARGIN_H, // x = 10.0 + PLAYAREA_MARGIN_V, // y = 10.0 + Game::WIDTH - 2.0f * PLAYAREA_MARGIN_H, // width = 620.0 + Game::HEIGHT - PLAYAREA_MARGIN_V - SCOREBOARD_H // height = 406.0 +}; + +// Zona de marcador +// Ocupa: tot l'ample, 64px d'alçada en la part inferior +constexpr SDL_FRect SCOREBOARD = { + 0.0f, // x = 0.0 + Game::HEIGHT - SCOREBOARD_H, // y = 416.0 + static_cast(Game::WIDTH), // width = 640.0 + SCOREBOARD_H // height = 64.0 +}; +} // namespace Zones + +// Objetos del juego +namespace Entities { +constexpr int MAX_ORNIS = 15; +constexpr int MAX_BALES = 3; +constexpr int MAX_IPUNTS = 30; + +constexpr float SHIP_RADIUS = 12.0f; +constexpr float ENEMY_RADIUS = 20.0f; +constexpr float BULLET_RADIUS = 5.0f; +} // namespace Entities + +// Física (valores actuales del juego, sincronizados con joc_asteroides.cpp) +namespace Physics { +constexpr float ROTATION_SPEED = 3.14f; // rad/s (~180°/s) +constexpr float ACCELERATION = 400.0f; // px/s² +constexpr float MAX_VELOCITY = 120.0f; // px/s +constexpr float FRICTION = 20.0f; // px/s² +constexpr float ENEMY_SPEED = 2.0f; // unidades/frame +constexpr float BULLET_SPEED = 6.0f; // unidades/frame +constexpr float VELOCITY_SCALE = 20.0f; // factor conversión frame→tiempo + +// Explosions (debris physics) +namespace Debris { +constexpr float VELOCITAT_BASE = 80.0f; // Velocitat inicial (px/s) +constexpr float VARIACIO_VELOCITAT = 40.0f; // ±variació aleatòria (px/s) +constexpr float ACCELERACIO = -60.0f; // Fricció/desacceleració (px/s²) +constexpr float ROTACIO_MIN = 0.1f; // Rotació mínima (rad/s ~5.7°/s) +constexpr float ROTACIO_MAX = 0.3f; // Rotació màxima (rad/s ~17.2°/s) +constexpr float TEMPS_VIDA = 2.0f; // Duració màxima (segons) +constexpr float SHRINK_RATE = 0.5f; // Reducció de mida (factor/s) +} // namespace Debris +} // namespace Physics + +// Matemáticas +namespace Math { +constexpr float PI = 3.14159265359f; +} // namespace Math + +// Colores (oscilación para efecto CRT) +namespace Color { +// Frecuencia de oscilación +constexpr float FREQUENCY = 6.0f; // 1 Hz (1 ciclo/segundo) + +// Color de líneas (efecto fósforo verde CRT) +constexpr uint8_t LINE_MIN_R = 100; // Verde oscuro +constexpr uint8_t LINE_MIN_G = 200; +constexpr uint8_t LINE_MIN_B = 100; + +constexpr uint8_t LINE_MAX_R = 100; // Verde brillante +constexpr uint8_t LINE_MAX_G = 255; +constexpr uint8_t LINE_MAX_B = 100; + +// Color de fondo (pulso sutil verde oscuro) +constexpr uint8_t BACKGROUND_MIN_R = 0; // Negro +constexpr uint8_t BACKGROUND_MIN_G = 5; +constexpr uint8_t BACKGROUND_MIN_B = 0; + +constexpr uint8_t BACKGROUND_MAX_R = 0; // Verde muy oscuro +constexpr uint8_t BACKGROUND_MAX_G = 15; +constexpr uint8_t BACKGROUND_MAX_B = 0; +} // namespace Color + +// Brillantor (control de intensitat per cada tipus d'entitat) +namespace Brightness { +// Brillantor estàtica per entitats de joc (0.0-1.0) +constexpr float NAU = 1.0f; // Màxima visibilitat (jugador) +constexpr float ENEMIC = 0.7f; // 30% més tènue (destaca menys) +constexpr float BALA = 0.9f; // Destacada però menys que nau + +// Starfield: gradient segons distància al centre +// distancia_centre: 0.0 (centre) → 1.0 (vora pantalla) +// brightness = MIN + (MAX - MIN) * distancia_centre +constexpr float STARFIELD_MIN = 0.3f; // Estrelles llunyanes (prop del centre) +constexpr float STARFIELD_MAX = 0.8f; // Estrelles properes (vora pantalla) +} // namespace Brightness + +// Renderització (V-Sync i altres opcions de render) +namespace Rendering { +constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled +} // namespace Rendering + +// Audio (sistema de so i música) +namespace Audio { +constexpr float VOLUME = 1.0F; // Volumen maestro (0.0 a 1.0) +constexpr bool ENABLED = true; // Audio habilitado por defecto +} // namespace Audio + +// Música (pistas de fondo) +namespace Music { +constexpr float VOLUME = 0.8F; // Volumen música +constexpr bool ENABLED = true; // Música habilitada +constexpr const char* GAME_TRACK = "game.ogg"; // Pista de juego +constexpr int FADE_DURATION_MS = 1000; // Fade out duration +} // namespace Music + +// Efectes de so (sons puntuals) +namespace Sound { +constexpr float VOLUME = 1.0F; // Volumen efectos +constexpr bool ENABLED = true; // Sonidos habilitados +constexpr const char* EXPLOSION = "explosion.wav"; // Explosión +constexpr const char* LASER = "laser_shoot.wav"; // Disparo +} // namespace Sound +} // namespace Defaults diff --git a/source/core/graphics/shape.cpp b/source/core/graphics/shape.cpp new file mode 100644 index 0000000..6e194d2 --- /dev/null +++ b/source/core/graphics/shape.cpp @@ -0,0 +1,155 @@ +// shape.cpp - Implementació del sistema de formes vectorials +// © 2025 Port a C++20 amb SDL3 + +#include "core/graphics/shape.hpp" + +#include +#include +#include +#include + +namespace Graphics { + +Shape::Shape(const std::string& filepath) + : centre_({0.0f, 0.0f}), + escala_defecte_(1.0f), + nom_("unnamed") { + carregar(filepath); +} + +bool Shape::carregar(const std::string& filepath) { + // Llegir fitxer + std::ifstream file(filepath); + if (!file.is_open()) { + std::cerr << "[Shape] Error: no es pot obrir " << filepath << std::endl; + return false; + } + + // Llegir tot el contingut + std::stringstream buffer; + buffer << file.rdbuf(); + std::string contingut = buffer.str(); + file.close(); + + // Parsejar + return parsejar_fitxer(contingut); +} + +bool Shape::parsejar_fitxer(const std::string& contingut) { + std::istringstream iss(contingut); + std::string line; + + while (std::getline(iss, line)) { + // Trim whitespace + line = trim(line); + + // Skip comments and blanks + if (line.empty() || line[0] == '#') + continue; + + // Parse command + if (starts_with(line, "name:")) { + nom_ = trim(extract_value(line)); + } else if (starts_with(line, "scale:")) { + try { + escala_defecte_ = std::stof(extract_value(line)); + } catch (...) { + std::cerr << "[Shape] Warning: escala invàlida, usant 1.0" << std::endl; + escala_defecte_ = 1.0f; + } + } else if (starts_with(line, "center:")) { + parse_center(extract_value(line)); + } else if (starts_with(line, "polyline:")) { + auto points = parse_points(extract_value(line)); + if (points.size() >= 2) { + primitives_.push_back({PrimitiveType::POLYLINE, points}); + } else { + std::cerr << "[Shape] Warning: polyline amb menys de 2 punts ignorada" + << std::endl; + } + } else if (starts_with(line, "line:")) { + auto points = parse_points(extract_value(line)); + if (points.size() == 2) { + primitives_.push_back({PrimitiveType::LINE, points}); + } else { + std::cerr << "[Shape] Warning: line ha de tenir exactament 2 punts" + << std::endl; + } + } + // Comandes desconegudes ignorades silenciosament + } + + if (primitives_.empty()) { + std::cerr << "[Shape] Error: cap primitiva carregada" << std::endl; + return false; + } + + return true; +} + +// Helper: trim whitespace +std::string Shape::trim(const std::string& str) const { + const char* whitespace = " \t\n\r"; + size_t start = str.find_first_not_of(whitespace); + if (start == std::string::npos) + return ""; + + size_t end = str.find_last_not_of(whitespace); + return str.substr(start, end - start + 1); +} + +// Helper: starts_with +bool Shape::starts_with(const std::string& str, + const std::string& prefix) const { + if (str.length() < prefix.length()) + return false; + return str.compare(0, prefix.length(), prefix) == 0; +} + +// Helper: extract value after ':' +std::string Shape::extract_value(const std::string& line) const { + size_t colon = line.find(':'); + if (colon == std::string::npos) + return ""; + return line.substr(colon + 1); +} + +// Helper: parse center "x, y" +void Shape::parse_center(const std::string& value) { + std::string val = trim(value); + size_t comma = val.find(','); + if (comma != std::string::npos) { + try { + centre_.x = std::stof(trim(val.substr(0, comma))); + centre_.y = std::stof(trim(val.substr(comma + 1))); + } catch (...) { + std::cerr << "[Shape] Warning: centre invàlid, usant (0,0)" << std::endl; + centre_ = {0.0f, 0.0f}; + } + } +} + +// Helper: parse points "x1,y1 x2,y2 x3,y3" +std::vector Shape::parse_points(const std::string& str) const { + std::vector points; + std::istringstream iss(trim(str)); + std::string pair; + + while (iss >> pair) { // Whitespace-separated + size_t comma = pair.find(','); + if (comma != std::string::npos) { + try { + float x = std::stof(pair.substr(0, comma)); + float y = std::stof(pair.substr(comma + 1)); + points.push_back({x, y}); + } catch (...) { + std::cerr << "[Shape] Warning: punt invàlid ignorat: " << pair + << std::endl; + } + } + } + + return points; +} + +} // namespace Graphics diff --git a/source/core/graphics/shape.hpp b/source/core/graphics/shape.hpp new file mode 100644 index 0000000..1f5ab7a --- /dev/null +++ b/source/core/graphics/shape.hpp @@ -0,0 +1,64 @@ +// shape.hpp - Sistema de formes vectorials +// © 2025 Port a C++20 amb SDL3 + +#pragma once + +#include +#include + +#include "core/types.hpp" + +namespace Graphics { + +// Tipus de primitiva dins d'una forma +enum class PrimitiveType { + POLYLINE, // Seqüència de punts connectats + LINE // Línia individual (2 punts) +}; + +// Primitiva individual (polyline o line) +struct ShapePrimitive { + PrimitiveType type; + std::vector points; // 2+ punts per polyline, exactament 2 per line +}; + +// Classe Shape - representa una forma vectorial carregada des de .shp +class Shape { + public: + // Constructors + Shape() = default; + explicit Shape(const std::string& filepath); + + // Carregar forma des de fitxer .shp + bool carregar(const std::string& filepath); + + // Getters + const std::vector& get_primitives() const { + return primitives_; + } + const Punt& get_centre() const { return centre_; } + float get_escala_defecte() const { return escala_defecte_; } + bool es_valida() const { return !primitives_.empty(); } + + // Info de depuració + std::string get_nom() const { return nom_; } + size_t get_num_primitives() const { return primitives_.size(); } + + private: + std::vector primitives_; + Punt centre_; // Centre/origen de la forma + float escala_defecte_; // Escala per defecte (normalment 1.0) + std::string nom_; // Nom de la forma (per depuració) + + // Parsejador del fitxer + bool parsejar_fitxer(const std::string& contingut); + + // Helpers privats per parsejar + std::string trim(const std::string& str) const; + bool starts_with(const std::string& str, const std::string& prefix) const; + std::string extract_value(const std::string& line) const; + void parse_center(const std::string& value); + std::vector parse_points(const std::string& str) const; +}; + +} // namespace Graphics diff --git a/source/core/graphics/shape_loader.cpp b/source/core/graphics/shape_loader.cpp new file mode 100644 index 0000000..c3edb5e --- /dev/null +++ b/source/core/graphics/shape_loader.cpp @@ -0,0 +1,72 @@ +// shape_loader.cpp - Implementació del carregador amb caché +// © 2025 Port a C++20 amb SDL3 + +#include "core/graphics/shape_loader.hpp" + +#include + +namespace Graphics { + +// Inicialització de variables estàtiques +std::unordered_map> ShapeLoader::cache_; +std::string ShapeLoader::base_path_ = "data/shapes/"; + +std::shared_ptr ShapeLoader::load(const std::string& filename) { + // Check cache first + auto it = cache_.find(filename); + if (it != cache_.end()) { + std::cout << "[ShapeLoader] Cache hit: " << filename << std::endl; + return it->second; // Cache hit + } + + // Resolve full path + std::string fullpath = resolve_path(filename); + + // Create and load shape + auto shape = std::make_shared(); + if (!shape->carregar(fullpath)) { + std::cerr << "[ShapeLoader] Error: no s'ha pogut carregar " << filename + << std::endl; + return nullptr; + } + + // Verify shape is valid + if (!shape->es_valida()) { + std::cerr << "[ShapeLoader] Error: forma invàlida " << filename + << std::endl; + return nullptr; + } + + // Cache and return + std::cout << "[ShapeLoader] Carregat: " << filename << " (" + << shape->get_nom() << ", " << shape->get_num_primitives() + << " primitives)" << std::endl; + + cache_[filename] = shape; + return shape; +} + +void ShapeLoader::clear_cache() { + std::cout << "[ShapeLoader] Netejant caché (" << cache_.size() << " formes)" + << std::endl; + cache_.clear(); +} + +size_t ShapeLoader::get_cache_size() { return cache_.size(); } + +std::string ShapeLoader::resolve_path(const std::string& filename) { + // Si és un path absolut (comença amb '/'), usar-lo directament + if (!filename.empty() && filename[0] == '/') { + return filename; + } + + // Si ja conté el prefix base_path, usar-lo directament + if (filename.find(base_path_) == 0) { + return filename; + } + + // Altrament, afegir base_path (ara suporta subdirectoris) + return base_path_ + filename; +} + +} // namespace Graphics diff --git a/source/core/graphics/shape_loader.hpp b/source/core/graphics/shape_loader.hpp new file mode 100644 index 0000000..d606c0a --- /dev/null +++ b/source/core/graphics/shape_loader.hpp @@ -0,0 +1,39 @@ +// shape_loader.hpp - Carregador estàtic de formes amb caché +// © 2025 Port a C++20 amb SDL3 + +#pragma once + +#include +#include +#include + +#include "core/graphics/shape.hpp" + +namespace Graphics { + +// Carregador estàtic de formes amb caché +class ShapeLoader { + public: + // No instanciable (tot estàtic) + ShapeLoader() = delete; + + // Carregar forma des de fitxer (amb caché) + // Retorna punter compartit (nullptr si error) + // Exemple: load("ship.shp") → busca a "data/shapes/ship.shp" + static std::shared_ptr load(const std::string& filename); + + // Netejar caché (útil per debug/recàrrega) + static void clear_cache(); + + // Estadístiques (debug) + static size_t get_cache_size(); + + private: + static std::unordered_map> cache_; + static std::string base_path_; // "data/shapes/" + + // Helpers privats + static std::string resolve_path(const std::string& filename); +}; + +} // namespace Graphics diff --git a/source/core/graphics/starfield.cpp b/source/core/graphics/starfield.cpp new file mode 100644 index 0000000..ad97e0e --- /dev/null +++ b/source/core/graphics/starfield.cpp @@ -0,0 +1,163 @@ +// starfield.cpp - Implementació del sistema d'estrelles de fons +// © 2025 Orni Attack + +#include "core/graphics/starfield.hpp" + +#include +#include +#include + +#include "core/defaults.hpp" +#include "core/rendering/shape_renderer.hpp" + +namespace Graphics { + +// Constructor +Starfield::Starfield(SDL_Renderer* renderer, + const Punt& punt_fuga, + const SDL_FRect& area, + int densitat) + : renderer_(renderer) + , punt_fuga_(punt_fuga) + , area_(area) + , densitat_(densitat) { + // Carregar forma d'estrella + shape_estrella_ = std::make_shared("data/shapes/star.shp"); + + if (!shape_estrella_->es_valida()) { + std::cerr << "ERROR: No s'ha pogut carregar data/shapes/star.shp" << std::endl; + return; + } + + // Configurar 3 capes amb diferents velocitats i escales + // Capa 0: Fons llunyà (lenta, petita) + capes_.push_back({20.0f, 0.3f, 0.8f, densitat / 3}); + + // Capa 1: Profunditat mitjana + capes_.push_back({40.0f, 0.5f, 1.2f, densitat / 3}); + + // Capa 2: Primer pla (ràpida, gran) + capes_.push_back({80.0f, 0.8f, 2.0f, densitat / 3}); + + // Calcular radi màxim (distància del centre al racó més llunyà) + float dx = std::max(punt_fuga_.x, area_.w - punt_fuga_.x); + float dy = std::max(punt_fuga_.y, area_.h - punt_fuga_.y); + radi_max_ = std::sqrt(dx * dx + dy * dy); + + // Inicialitzar estrelles amb posicions distribuïdes (pre-omplir pantalla) + for (int capa_idx = 0; capa_idx < 3; capa_idx++) { + int num = capes_[capa_idx].num_estrelles; + for (int i = 0; i < num; i++) { + Estrella estrella; + estrella.capa = capa_idx; + + // Angle aleatori + estrella.angle = (static_cast(rand()) / RAND_MAX) * 2.0f * Defaults::Math::PI; + + // Distància aleatòria (0.0 a 1.0) per omplir tota la pantalla + estrella.distancia_centre = static_cast(rand()) / RAND_MAX; + + // Calcular posició des de la distància + float radi = estrella.distancia_centre * radi_max_; + estrella.posicio.x = punt_fuga_.x + radi * std::cos(estrella.angle); + estrella.posicio.y = punt_fuga_.y + radi * std::sin(estrella.angle); + + estrelles_.push_back(estrella); + } + } +} + +// Inicialitzar una estrella (nova o regenerada) +void Starfield::inicialitzar_estrella(Estrella& estrella) { + // Angle aleatori des del punt de fuga cap a fora + estrella.angle = (static_cast(rand()) / RAND_MAX) * 2.0f * Defaults::Math::PI; + + // Distància inicial petita (5% del radi màxim) - neix prop del centre + estrella.distancia_centre = 0.05f; + + // Posició inicial: molt prop del punt de fuga + float radi = estrella.distancia_centre * radi_max_; + estrella.posicio.x = punt_fuga_.x + radi * std::cos(estrella.angle); + estrella.posicio.y = punt_fuga_.y + radi * std::sin(estrella.angle); +} + +// Verificar si una estrella està fora de l'àrea +bool Starfield::fora_area(const Estrella& estrella) const { + return (estrella.posicio.x < area_.x || + estrella.posicio.x > area_.x + area_.w || + estrella.posicio.y < area_.y || + estrella.posicio.y > area_.y + area_.h); +} + +// Calcular escala dinàmica segons distància del centre +float Starfield::calcular_escala(const Estrella& estrella) const { + const CapaConfig& capa = capes_[estrella.capa]; + + // Interpolació lineal basada en distància del centre + // distancia_centre: 0.0 (centre) → 1.0 (vora) + return capa.escala_min + + (capa.escala_max - capa.escala_min) * estrella.distancia_centre; +} + +// Calcular brightness dinàmica segons distància del centre +float Starfield::calcular_brightness(const Estrella& estrella) const { + // Interpolació lineal: estrelles properes (vora) més brillants + // distancia_centre: 0.0 (centre, llunyanes) → 1.0 (vora, properes) + return Defaults::Brightness::STARFIELD_MIN + + (Defaults::Brightness::STARFIELD_MAX - Defaults::Brightness::STARFIELD_MIN) * + estrella.distancia_centre; +} + +// Actualitzar posicions de les estrelles +void Starfield::actualitzar(float delta_time) { + for (auto& estrella : estrelles_) { + // Obtenir configuració de la capa + const CapaConfig& capa = capes_[estrella.capa]; + + // Moure cap a fora des del centre + float velocitat = capa.velocitat_base; + float dx = velocitat * std::cos(estrella.angle) * delta_time; + float dy = velocitat * std::sin(estrella.angle) * delta_time; + + estrella.posicio.x += dx; + estrella.posicio.y += dy; + + // Actualitzar distància del centre + float dx_centre = estrella.posicio.x - punt_fuga_.x; + float dy_centre = estrella.posicio.y - punt_fuga_.y; + float dist_px = std::sqrt(dx_centre * dx_centre + dy_centre * dy_centre); + estrella.distancia_centre = dist_px / radi_max_; + + // Si ha sortit de l'àrea, regenerar-la + if (fora_area(estrella)) { + inicialitzar_estrella(estrella); + } + } +} + +// Dibuixar totes les estrelles +void Starfield::dibuixar() { + if (!shape_estrella_->es_valida()) { + return; + } + + for (const auto& estrella : estrelles_) { + // Calcular escala i brightness dinàmicament + float escala = calcular_escala(estrella); + float brightness = calcular_brightness(estrella); + + // Renderitzar estrella sense rotació + Rendering::render_shape( + renderer_, + shape_estrella_, + estrella.posicio, + 0.0f, // angle (les estrelles no giren) + escala, // escala dinàmica + true, // dibuixar + 1.0f, // progress (sempre visible) + brightness // brightness dinàmica + ); + } +} + +} // namespace Graphics diff --git a/source/core/graphics/starfield.hpp b/source/core/graphics/starfield.hpp new file mode 100644 index 0000000..40b5df4 --- /dev/null +++ b/source/core/graphics/starfield.hpp @@ -0,0 +1,80 @@ +// starfield.hpp - Sistema d'estrelles de fons amb efecte de profunditat +// © 2025 Orni Attack + +#pragma once + +#include + +#include +#include + +#include "core/graphics/shape.hpp" +#include "core/types.hpp" + +namespace Graphics { + +// Configuració per cada capa de profunditat +struct CapaConfig { + float velocitat_base; // Velocitat base d'aquesta capa (px/s) + float escala_min; // Escala mínima prop del centre + float escala_max; // Escala màxima al límit de pantalla + int num_estrelles; // Nombre d'estrelles en aquesta capa +}; + +// Classe Starfield - camp d'estrelles animat amb efecte de profunditat +class Starfield { + public: + // Constructor + // - renderer: SDL renderer + // - punt_fuga: punt d'origen/fuga des d'on surten les estrelles + // - area: rectangle on actuen les estrelles (SDL_FRect) + // - densitat: nombre total d'estrelles (es divideix entre capes) + Starfield(SDL_Renderer* renderer, + const Punt& punt_fuga, + const SDL_FRect& area, + int densitat = 150); + + // Actualitzar posicions de les estrelles + void actualitzar(float delta_time); + + // Dibuixar totes les estrelles + void dibuixar(); + + // Setters per ajustar paràmetres en temps real + void set_punt_fuga(const Punt& punt) { punt_fuga_ = punt; } + + private: + // Estructura interna per cada estrella + struct Estrella { + Punt posicio; // Posició actual + float angle; // Angle de moviment (radians) + float distancia_centre; // Distància normalitzada del centre (0.0-1.0) + int capa; // Índex de capa (0=lluny, 1=mitjà, 2=prop) + }; + + // Inicialitzar una estrella (nova o regenerada) + void inicialitzar_estrella(Estrella& estrella); + + // Verificar si una estrella està fora de l'àrea + bool fora_area(const Estrella& estrella) const; + + // Calcular escala dinàmica segons distància del centre + float calcular_escala(const Estrella& estrella) const; + + // Calcular brightness dinàmica segons distància del centre + float calcular_brightness(const Estrella& estrella) const; + + // Dades + std::vector estrelles_; + std::vector capes_; // Configuració de les 3 capes + std::shared_ptr shape_estrella_; + SDL_Renderer* renderer_; + + // Configuració + Punt punt_fuga_; // Punt d'origen de les estrelles + SDL_FRect area_; // Àrea activa + float radi_max_; // Distància màxima del centre al límit de pantalla + int densitat_; // Nombre total d'estrelles +}; + +} // namespace Graphics diff --git a/source/core/graphics/vector_text.cpp b/source/core/graphics/vector_text.cpp new file mode 100644 index 0000000..efb7d18 --- /dev/null +++ b/source/core/graphics/vector_text.cpp @@ -0,0 +1,263 @@ +// vector_text.cpp - Implementació del sistema de text vectorial +// © 2025 Port a C++20 amb SDL3 + +#include "core/graphics/vector_text.hpp" + +#include + +#include "core/graphics/shape_loader.hpp" +#include "core/rendering/shape_renderer.hpp" + +namespace Graphics { + +// Constants per a mides base dels caràcters +constexpr float char_width = 20.0f; // Amplada base del caràcter +constexpr float char_height = 40.0f; // Altura base del caràcter + +VectorText::VectorText(SDL_Renderer* renderer) + : renderer_(renderer) { + load_charset(); +} + +void VectorText::load_charset() { + // Cargar dígitos 0-9 + for (char c = '0'; c <= '9'; c++) { + std::string filename = get_shape_filename(c); + auto shape = ShapeLoader::load(filename); + + if (shape && shape->es_valida()) { + chars_[c] = shape; + } else { + std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename + << std::endl; + } + } + + // Cargar lletres A-Z (majúscules) + for (char c = 'A'; c <= 'Z'; c++) { + std::string filename = get_shape_filename(c); + auto shape = ShapeLoader::load(filename); + + if (shape && shape->es_valida()) { + chars_[c] = shape; + } else { + std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename + << std::endl; + } + } + + // Cargar símbolos + const std::string symbols[] = {".", ",", "-", ":", "!", "?"}; + for (const auto& sym : symbols) { + char c = sym[0]; + std::string filename = get_shape_filename(c); + auto shape = ShapeLoader::load(filename); + + if (shape && shape->es_valida()) { + chars_[c] = shape; + } else { + std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename + << std::endl; + } + } + + // Cargar símbolo de copyright (©) - UTF-8 U+00A9 + // Usem el segon byte (0xA9) com a key interna + { + char c = '\xA9'; // 169 decimal + std::string filename = "font/char_copyright.shp"; + auto shape = ShapeLoader::load(filename); + + if (shape && shape->es_valida()) { + chars_[c] = shape; + } else { + std::cerr << "[VectorText] Warning: no s'ha pogut carregar " << filename + << std::endl; + } + } + + std::cout << "[VectorText] Carregats " << chars_.size() << " caràcters" + << std::endl; +} + +std::string VectorText::get_shape_filename(char c) const { + // Mapeo carácter → nombre de archivo (amb prefix "font/") + switch (c) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return std::string("font/char_") + c + ".shp"; + + // Lletres majúscules A-Z + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + case 'G': + case 'H': + case 'I': + case 'J': + case 'K': + case 'L': + case 'M': + case 'N': + case 'O': + case 'P': + case 'Q': + case 'R': + case 'S': + case 'T': + case 'U': + case 'V': + case 'W': + case 'X': + case 'Y': + case 'Z': + return std::string("font/char_") + c + ".shp"; + + // Lletres minúscules a-z (convertir a majúscules) + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + case 'g': + case 'h': + case 'i': + case 'j': + case 'k': + case 'l': + case 'm': + case 'n': + case 'o': + case 'p': + case 'q': + case 'r': + case 's': + case 't': + case 'u': + case 'v': + case 'w': + case 'x': + case 'y': + case 'z': + return std::string("font/char_") + char(c - 32) + ".shp"; + + // Símbols + case '.': + return "font/char_dot.shp"; + case ',': + return "font/char_comma.shp"; + case '-': + return "font/char_minus.shp"; + case ':': + return "font/char_colon.shp"; + case '!': + return "font/char_exclamation.shp"; + case '?': + return "font/char_question.shp"; + case ' ': + return ""; // Espai es maneja sense carregar shape + + case '\xA9': // Copyright symbol (©) - UTF-8 U+00A9 + return "font/char_copyright.shp"; + + default: + return ""; // Caràcter no suportat + } +} + +bool VectorText::is_supported(char c) const { + return chars_.find(c) != chars_.end(); +} + +void VectorText::render(const std::string& text, const Punt& posicio, float escala, float spacing) { + if (!renderer_) { + return; + } + + // Ancho de un carácter base (20 px a escala 1.0) + const float char_width_scaled = char_width * escala; + + // Spacing escalado + const float spacing_scaled = spacing * escala; + + // Altura de un carácter escalado (necesario para ajustar Y) + const float char_height_scaled = char_height * escala; + + // Posición actual del centro del carácter (ajustada desde esquina superior + // izquierda) + float current_x = posicio.x; + + // Iterar sobre cada byte del string (con detecció UTF-8) + for (size_t i = 0; i < text.length(); i++) { + unsigned char c = static_cast(text[i]); + + // Detectar copyright UTF-8 (0xC2 0xA9) + if (c == 0xC2 && i + 1 < text.length() && + static_cast(text[i + 1]) == 0xA9) { + c = 0xA9; // Usar segon byte com a key + i++; // Saltar el següent byte + } + + // Manejar espacios (avanzar sin dibujar) + if (c == ' ') { + current_x += char_width_scaled + spacing_scaled; + continue; + } + + // Verificar si el carácter está soportado + auto it = chars_.find(c); + if (it != chars_.end()) { + // Renderizar carácter + // Ajustar Y para que posicio represente esquina superior izquierda + // (render_shape espera el centro, así que sumamos la mitad de la altura) + Punt char_pos = {current_x, posicio.y + char_height_scaled / 2.0f}; + Rendering::render_shape(renderer_, it->second, char_pos, 0.0f, escala, true); + + // Avanzar posición + current_x += char_width_scaled + spacing_scaled; + } else { + // Carácter no soportado: saltar (o renderizar '?' en el futuro) + std::cerr << "[VectorText] Warning: caràcter no suportat '" << c << "'" + << std::endl; + current_x += char_width_scaled + spacing_scaled; + } + } +} + +float VectorText::get_text_width(const std::string& text, float escala, float spacing) const { + if (text.empty()) { + return 0.0f; + } + + const float char_width_scaled = char_width * escala; + const float spacing_scaled = spacing * escala; + + // Ancho total = (número de caracteres × char_width) + (espacios entre + // caracteres) + float width = text.length() * char_width_scaled; + + // Añadir spacing entre caracteres (n-1 espacios para n caracteres) + if (text.length() > 1) { + width += (text.length() - 1) * spacing_scaled; + } + + return width; +} + +float VectorText::get_text_height(float escala) const { + return char_height * escala; +} + +} // namespace Graphics diff --git a/source/core/graphics/vector_text.hpp b/source/core/graphics/vector_text.hpp new file mode 100644 index 0000000..50c6760 --- /dev/null +++ b/source/core/graphics/vector_text.hpp @@ -0,0 +1,46 @@ +// vector_text.hpp - Sistema de texto vectorial con display de 7-segmentos +// © 2025 Port a C++20 amb SDL3 + +#pragma once + +#include + +#include +#include +#include + +#include "core/graphics/shape.hpp" +#include "core/types.hpp" + +namespace Graphics { + +class VectorText { + public: + VectorText(SDL_Renderer* renderer); + + // Renderizar string completo + // - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':', + // '!', '?', ' ') + // - posicio: posición inicial (esquina superior izquierda) + // - escala: factor de escala (1.0 = 20×40 px por carácter) + // - spacing: espacio entre caracteres en píxeles (a escala 1.0) + void render(const std::string& text, const Punt& posicio, float escala = 1.0f, float spacing = 2.0f); + + // Calcular ancho total de un string (útil para centrado) + float get_text_width(const std::string& text, float escala = 1.0f, float spacing = 2.0f) const; + + // Calcular altura del texto (útil para centrado vertical) + float get_text_height(float escala = 1.0f) const; + + // Verificar si un carácter está soportado + bool is_supported(char c) const; + + private: + SDL_Renderer* renderer_; + std::unordered_map> chars_; + + void load_charset(); + std::string get_shape_filename(char c) const; +}; + +} // namespace Graphics diff --git a/source/core/input/mouse.cpp b/source/core/input/mouse.cpp new file mode 100644 index 0000000..ff4f2db --- /dev/null +++ b/source/core/input/mouse.cpp @@ -0,0 +1,63 @@ +#include "core/input/mouse.hpp" + +namespace Mouse { +Uint32 cursor_hide_time = 3000; // Tiempo en milisegundos para ocultar el cursor +Uint32 last_mouse_move_time = 0; // Última vez que el ratón se movió +bool cursor_visible = true; // Estado del cursor + +// Modo forzado: Usado cuando SDLManager entra en pantalla completa. +// Cuando está activado, el cursor permanece oculto independientemente del movimiento del ratón. +// SDLManager controla esto mediante llamadas a setForceHidden(). +bool force_hidden = false; + +void setForceHidden(bool force) { + force_hidden = force; + + if (force) { + // Entrando en modo oculto forzado: ocultar cursor inmediatamente + if (cursor_visible) { + SDL_HideCursor(); + cursor_visible = false; + } + } else { + // Saliendo de modo oculto forzado: mostrar cursor y resetear temporizador + SDL_ShowCursor(); + cursor_visible = true; + last_mouse_move_time = SDL_GetTicks(); // Resetear temporizador + } +} + +bool isForceHidden() { + return force_hidden; +} + +void handleEvent(const SDL_Event& event) { + // CRÍTICO: Si estamos en modo forzado, ignorar todos los eventos del ratón + if (force_hidden) { + return; // Salir temprano - no procesar ningún evento + } + + // MODO NORMAL: Mostrar cursor al mover el ratón + if (event.type == SDL_EVENT_MOUSE_MOTION) { + last_mouse_move_time = SDL_GetTicks(); + if (!cursor_visible) { + SDL_ShowCursor(); + cursor_visible = true; + } + } +} + +void updateCursorVisibility() { + // CRÍTICO: Si estamos en modo forzado, no aplicar lógica de timeout + if (force_hidden) { + return; // Salir temprano - el cursor permanece oculto + } + + // MODO NORMAL: Auto-ocultar basado en timeout + Uint32 current_time = SDL_GetTicks(); + if (cursor_visible && (current_time - last_mouse_move_time > cursor_hide_time)) { + SDL_HideCursor(); + cursor_visible = false; + } +} +} // namespace Mouse diff --git a/source/core/input/mouse.hpp b/source/core/input/mouse.hpp new file mode 100644 index 0000000..df8e5ac --- /dev/null +++ b/source/core/input/mouse.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace Mouse { +extern Uint32 cursor_hide_time; // Tiempo en milisegundos para ocultar el cursor +extern Uint32 last_mouse_move_time; // Última vez que el ratón se movió +extern bool cursor_visible; // Estado del cursor + +void handleEvent(const SDL_Event& event); +void updateCursorVisibility(); + +// Control de visibilidad forzada (para modo pantalla completa) +void setForceHidden(bool force); // Activar/desactivar ocultación forzada +bool isForceHidden(); // Consultar estado actual +} // namespace Mouse diff --git a/source/core/rendering/color_oscillator.cpp b/source/core/rendering/color_oscillator.cpp new file mode 100644 index 0000000..792a1d0 --- /dev/null +++ b/source/core/rendering/color_oscillator.cpp @@ -0,0 +1,68 @@ +// color_oscillator.cpp - Implementació d'oscil·lació de color +// © 2025 Port a C++20 amb SDL3 + +#include "core/rendering/color_oscillator.hpp" + +#include + +#include "core/defaults.hpp" + +namespace Rendering { + +ColorOscillator::ColorOscillator() + : accumulated_time_(0.0f) { + // Inicialitzar amb el color mínim + current_line_color_ = {Defaults::Color::LINE_MIN_R, + Defaults::Color::LINE_MIN_G, + Defaults::Color::LINE_MIN_B, + 255}; + current_background_color_ = {Defaults::Color::BACKGROUND_MIN_R, + Defaults::Color::BACKGROUND_MIN_G, + Defaults::Color::BACKGROUND_MIN_B, + 255}; +} + +void ColorOscillator::update(float delta_time) { + accumulated_time_ += delta_time; + + float factor = + calculateOscillationFactor(accumulated_time_, Defaults::Color::FREQUENCY); + + // Interpolar colors de línies + SDL_Color line_min = {Defaults::Color::LINE_MIN_R, + Defaults::Color::LINE_MIN_G, + Defaults::Color::LINE_MIN_B, + 255}; + SDL_Color line_max = {Defaults::Color::LINE_MAX_R, + Defaults::Color::LINE_MAX_G, + Defaults::Color::LINE_MAX_B, + 255}; + current_line_color_ = interpolateColor(line_min, line_max, factor); + + // Interpolar colors de fons + SDL_Color bg_min = {Defaults::Color::BACKGROUND_MIN_R, + Defaults::Color::BACKGROUND_MIN_G, + Defaults::Color::BACKGROUND_MIN_B, + 255}; + SDL_Color bg_max = {Defaults::Color::BACKGROUND_MAX_R, + Defaults::Color::BACKGROUND_MAX_G, + Defaults::Color::BACKGROUND_MAX_B, + 255}; + current_background_color_ = interpolateColor(bg_min, bg_max, factor); +} + +float ColorOscillator::calculateOscillationFactor(float time, float frequency) { + // Oscil·lació senoïdal: sin(t * freq * 2π) + // Mapejar de [-1, 1] a [0, 1] + float radians = time * frequency * 2.0f * Defaults::Math::PI; + return (std::sin(radians) + 1.0f) / 2.0f; +} + +SDL_Color ColorOscillator::interpolateColor(SDL_Color min, SDL_Color max, float factor) { + return {static_cast(min.r + (max.r - min.r) * factor), + static_cast(min.g + (max.g - min.g) * factor), + static_cast(min.b + (max.b - min.b) * factor), + 255}; +} + +} // namespace Rendering diff --git a/source/core/rendering/color_oscillator.hpp b/source/core/rendering/color_oscillator.hpp new file mode 100644 index 0000000..60ed60c --- /dev/null +++ b/source/core/rendering/color_oscillator.hpp @@ -0,0 +1,29 @@ +// color_oscillator.hpp - Sistema d'oscil·lació de color per efecte CRT +// © 2025 Port a C++20 amb SDL3 + +#pragma once +#include + +namespace Rendering { + +class ColorOscillator { + public: + ColorOscillator(); + + void update(float delta_time); + + SDL_Color getCurrentLineColor() const { return current_line_color_; } + SDL_Color getCurrentBackgroundColor() const { + return current_background_color_; + } + + private: + float accumulated_time_; + SDL_Color current_line_color_; + SDL_Color current_background_color_; + + static float calculateOscillationFactor(float time, float frequency); + static SDL_Color interpolateColor(SDL_Color min, SDL_Color max, float factor); +}; + +} // namespace Rendering diff --git a/source/core/rendering/coordinate_transform.cpp b/source/core/rendering/coordinate_transform.cpp new file mode 100644 index 0000000..1b25d1f --- /dev/null +++ b/source/core/rendering/coordinate_transform.cpp @@ -0,0 +1,11 @@ +// coordinate_transform.cpp - Inicialització de variables globals +// © 2025 Port a C++20 amb SDL3 + +#include "core/rendering/coordinate_transform.hpp" + +namespace Rendering { + +// Factor d'escala global (inicialitzat a 1.0 per defecte) +float g_current_scale_factor = 1.0f; + +} // namespace Rendering diff --git a/source/core/rendering/coordinate_transform.hpp b/source/core/rendering/coordinate_transform.hpp new file mode 100644 index 0000000..7bd859f --- /dev/null +++ b/source/core/rendering/coordinate_transform.hpp @@ -0,0 +1,31 @@ +// coordinate_transform.hpp - Transformació de coordenades lògiques a físiques +// © 2025 Port a C++20 amb SDL3 + +#pragma once + +#include + +namespace Rendering { + +// Factor d'escala global (actualitzat cada frame per SDLManager) +extern float g_current_scale_factor; + +// Transforma coordenada lògica a física amb arrodoniment +inline int transform_x(int logical_x, float scale) { + return static_cast(std::round(logical_x * scale)); +} + +inline int transform_y(int logical_y, float scale) { + return static_cast(std::round(logical_y * scale)); +} + +// Variant que usa el factor d'escala global +inline int transform_x(int logical_x) { + return transform_x(logical_x, g_current_scale_factor); +} + +inline int transform_y(int logical_y) { + return transform_y(logical_y, g_current_scale_factor); +} + +} // namespace Rendering diff --git a/source/core/rendering/line_renderer.cpp b/source/core/rendering/line_renderer.cpp new file mode 100644 index 0000000..12e584c --- /dev/null +++ b/source/core/rendering/line_renderer.cpp @@ -0,0 +1,101 @@ +// line_renderer.cpp - Implementació de renderitzat de línies +// © 1999 Visente i Sergi (versió Pascal) +// © 2025 Port a C++20 amb SDL3 + +#include "core/rendering/line_renderer.hpp" + +#include + +#include "core/rendering/coordinate_transform.hpp" + +namespace Rendering { + +// [NUEVO] Color global compartit (actualitzat per ColorOscillator via +// SDLManager) +SDL_Color g_current_line_color = {255, 255, 255, 255}; // Blanc inicial + +bool linea(SDL_Renderer* renderer, int x1, int y1, int x2, int y2, bool dibuixar, float brightness) { + // Algorisme de Bresenham per dibuixar línies + // Basat en el codi Pascal original + + // Helper function: retorna el signe d'un nombre + auto sign = [](int x) -> int { + if (x < 0) + return -1; + if (x > 0) + return 1; + return 0; + }; + + // Variables per a l'algorisme (no utilitzades fins Fase 10 - detecció de + // col·lisions) int x = x1, y = y1; int xs = x2 - x1; int ys = y2 - y1; int + // xm = sign(xs); int ym = sign(ys); xs = std::abs(xs); ys = std::abs(ys); + + // Suprimir warning de variable no usada + (void)sign; + + // Detecció de col·lisió (TODO per Fase 10) + // El codi Pascal original llegia pixels del framebuffer bit-packed + // i comptava col·lisions. Per ara, usem SDL_RenderDrawLine i retornem false. + bool colisio = false; + + // Dibuixar amb SDL3 (més eficient que Bresenham píxel a píxel) + if (dibuixar && renderer) { + // Transformar coordenades lògiques (640x480) a físiques (resolució real) + float scale = g_current_scale_factor; + int px1 = transform_x(x1, scale); + int py1 = transform_y(y1, scale); + int px2 = transform_x(x2, scale); + int py2 = transform_y(y2, scale); + + // Aplicar brightness al color oscil·lat global + SDL_Color color_final; + color_final.r = static_cast(g_current_line_color.r * brightness); + color_final.g = static_cast(g_current_line_color.g * brightness); + color_final.b = static_cast(g_current_line_color.b * brightness); + color_final.a = 255; + + SDL_SetRenderDrawColor(renderer, color_final.r, color_final.g, color_final.b, 255); + + // Renderitzar amb coordenades físiques + SDL_RenderLine(renderer, static_cast(px1), static_cast(py1), + static_cast(px2), static_cast(py2)); + } + + // Algorisme de Bresenham original (conservat per a futura detecció de + // col·lisió) + /* + if (xs > ys) { + // Línia plana (<45 graus) + int count = -(xs / 2); + while (x != x2) { + count = count + ys; + x = x + xm; + if (count > 0) { + y = y + ym; + count = count - xs; + } + // Aquí aniria la detecció de col·lisió píxel a píxel + } + } else { + // Línia pronunciada (>=45 graus) + int count = -(ys / 2); + while (y != y2) { + count = count + xs; + y = y + ym; + if (count > 0) { + x = x + xm; + count = count - ys; + } + // Aquí aniria la detecció de col·lisió píxel a píxel + } + } + */ + + return colisio; +} + +// [NUEVO] Establir el color global de les línies +void setLineColor(SDL_Color color) { g_current_line_color = color; } + +} // namespace Rendering diff --git a/source/core/rendering/line_renderer.hpp b/source/core/rendering/line_renderer.hpp new file mode 100644 index 0000000..0954eed --- /dev/null +++ b/source/core/rendering/line_renderer.hpp @@ -0,0 +1,16 @@ +// line_renderer.hpp - Renderitzat de línies +// © 1999 Visente i Sergi (versió Pascal) +// © 2025 Port a C++20 amb SDL3 + +#pragma once +#include + +namespace Rendering { +// Algorisme de Bresenham per dibuixar línies +// Retorna true si hi ha col·lisió (per Fase 10) +// brightness: factor de brillantor (0.0-1.0, default 1.0 = màxima brillantor) +bool linea(SDL_Renderer* renderer, int x1, int y1, int x2, int y2, bool dibuixar, float brightness = 1.0f); + +// [NUEVO] Establir el color global de les línies (oscil·lació) +void setLineColor(SDL_Color color); +} // namespace Rendering diff --git a/source/core/rendering/polygon_renderer.cpp b/source/core/rendering/polygon_renderer.cpp new file mode 100644 index 0000000..f4ca471 --- /dev/null +++ b/source/core/rendering/polygon_renderer.cpp @@ -0,0 +1,86 @@ +// polygon_renderer.cpp - Implementació de renderitzat de polígons +// © 1999 Visente i Sergi (versió Pascal) +// © 2025 Port a C++20 amb SDL3 +// +// ============================================================================== +// DEPRECATED: Use core/rendering/shape_renderer.cpp instead +// ============================================================================== + +#include "core/rendering/polygon_renderer.hpp" + +#include +#include + +#include "core/defaults.hpp" +#include "core/rendering/line_renderer.hpp" + +namespace Rendering { + +void rota_tri(SDL_Renderer* renderer, const Triangle& tri, float angul, float velocitat, bool dibuixar) { + // Rotar i dibuixar triangle (nau) + // Conversió de coordenades polars a cartesianes amb rotació + // Basat en el codi Pascal original: lines 271-284 + + // Convertir cada punt polar a cartesià + // x = (r + velocitat) * cos(angle_punt + angle_nau) + centre.x + // y = (r + velocitat) * sin(angle_punt + angle_nau) + centre.y + + int x1 = static_cast(std::round((tri.p1.r + velocitat) * + std::cos(tri.p1.angle + angul))) + + tri.centre.x; + + int y1 = static_cast(std::round((tri.p1.r + velocitat) * + std::sin(tri.p1.angle + angul))) + + tri.centre.y; + + int x2 = static_cast(std::round((tri.p2.r + velocitat) * + std::cos(tri.p2.angle + angul))) + + tri.centre.x; + + int y2 = static_cast(std::round((tri.p2.r + velocitat) * + std::sin(tri.p2.angle + angul))) + + tri.centre.y; + + int x3 = static_cast(std::round((tri.p3.r + velocitat) * + std::cos(tri.p3.angle + angul))) + + tri.centre.x; + + int y3 = static_cast(std::round((tri.p3.r + velocitat) * + std::sin(tri.p3.angle + angul))) + + tri.centre.y; + + // Dibuixar les 3 línies que formen el triangle + linea(renderer, x1, y1, x2, y2, dibuixar); + linea(renderer, x1, y1, x3, y3, dibuixar); + linea(renderer, x3, y3, x2, y2, dibuixar); +} + +void rota_pol(SDL_Renderer* renderer, const Poligon& pol, float angul, bool dibuixar) { + // Rotar i dibuixar polígon (enemics i bales) + // Conversió de coordenades polars a cartesianes amb rotació + // Basat en el codi Pascal original: lines 286-296 + + // Array temporal per emmagatzemar punts convertits a cartesianes + std::array xy; + + // Convertir cada punt polar a cartesià + for (uint8_t i = 0; i < pol.n; i++) { + xy[i].x = static_cast(std::round( + pol.ipuntx[i].r * std::cos(pol.ipuntx[i].angle + angul))) + + pol.centre.x; + + xy[i].y = static_cast(std::round( + pol.ipuntx[i].r * std::sin(pol.ipuntx[i].angle + angul))) + + pol.centre.y; + } + + // Dibuixar línies entre punts consecutius + for (uint8_t i = 0; i < pol.n - 1; i++) { + linea(renderer, xy[i].x, xy[i].y, xy[i + 1].x, xy[i + 1].y, dibuixar); + } + + // Tancar el polígon (últim punt → primer punt) + linea(renderer, xy[pol.n - 1].x, xy[pol.n - 1].y, xy[0].x, xy[0].y, dibuixar); +} + +} // namespace Rendering diff --git a/source/core/rendering/polygon_renderer.hpp b/source/core/rendering/polygon_renderer.hpp new file mode 100644 index 0000000..703a25a --- /dev/null +++ b/source/core/rendering/polygon_renderer.hpp @@ -0,0 +1,22 @@ +// polygon_renderer.hpp - Renderitzat de polígons polars +// © 1999 Visente i Sergi (versió Pascal) +// © 2025 Port a C++20 amb SDL3 +// +// ============================================================================== +// DEPRECATED: Use core/rendering/shape_renderer.hpp instead +// ============================================================================== +// This file is kept temporarily for chatarra_cosmica_ (Phase 10: explosions) +// TODO Phase 10: Replace with particle system or remove completely + +#pragma once +#include + +#include "core/types.hpp" + +namespace Rendering { +// Rotar i dibuixar triangle (nau) +void rota_tri(SDL_Renderer* renderer, const Triangle& tri, float angul, float velocitat, bool dibuixar); + +// Rotar i dibuixar polígon (enemics i bales) +void rota_pol(SDL_Renderer* renderer, const Poligon& pol, float angul, bool dibuixar); +} // namespace Rendering diff --git a/source/core/rendering/primitives.cpp b/source/core/rendering/primitives.cpp new file mode 100644 index 0000000..85f1ec7 --- /dev/null +++ b/source/core/rendering/primitives.cpp @@ -0,0 +1,66 @@ +// primitives.cpp - Implementació de funcions geomètriques +// © 1999 Visente i Sergi (versió Pascal) +// © 2025 Port a C++20 amb SDL3 +// +// ============================================================================== +// DEPRECATED: Use Shape system instead (.shp files + ShapeLoader) +// ============================================================================== + +#include "primitives.hpp" + +#include + +#include "core/defaults.hpp" + +float modul(const Punt& p) { + // Càlcul de la magnitud d'un vector: sqrt(x² + y²) + return std::sqrt(p.x * p.x + p.y * p.y); +} + +void diferencia(const Punt& o, const Punt& d, Punt& p) { + // Resta de vectors (origen - destí) + p.x = o.x - d.x; + p.y = o.y - d.y; +} + +int distancia(const Punt& o, const Punt& d) { + // Distància entre dos punts + Punt p; + diferencia(o, d, p); + return static_cast(std::round(modul(p))); +} + +float angle_punt(const Punt& p) { + // Càlcul de l'angle d'un punt (arctan) + if (p.y != 0) { + return std::atan(p.x / p.y); + } + return 0.0f; +} + +void crear_poligon_regular(Poligon& pol, uint8_t n, float r) { + // Crear un polígon regular amb n costats i radi r + // Distribueix els punts uniformement al voltant d'un cercle + + float interval = 2.0f * Defaults::Math::PI / n; + float act = 0.0f; + + for (uint8_t i = 0; i < n; i++) { + pol.ipuntx[i].r = r; + pol.ipuntx[i].angle = act; + act += interval; + } + + // Inicialitzar propietats del polígon + pol.centre.x = 320.0f; + pol.centre.y = 200.0f; + pol.angle = 0.0f; + // Convertir velocitat de px/frame a px/s: 2 px/frame × 20 FPS = 40 px/s + pol.velocitat = Defaults::Physics::ENEMY_SPEED * 20.0f; + pol.n = n; + // Convertir rotació de rad/frame a rad/s: 0.0785 rad/frame × 20 FPS = 1.57 + // rad/s (~90°/s) + pol.drotacio = 0.078539816f * 20.0f; + pol.rotacio = 0.0f; + pol.esta = true; +} diff --git a/source/core/rendering/primitives.hpp b/source/core/rendering/primitives.hpp new file mode 100644 index 0000000..e97acb3 --- /dev/null +++ b/source/core/rendering/primitives.hpp @@ -0,0 +1,32 @@ +// primitives.hpp - Funcions geomètriques bàsiques +// © 1999 Visente i Sergi (versió Pascal) +// © 2025 Port a C++20 amb SDL3 +// +// ============================================================================== +// DEPRECATED: Use Shape system instead (.shp files + ShapeLoader) +// ============================================================================== +// This file is kept temporarily for chatarra_cosmica_ (Phase 10: explosions) +// TODO Phase 10: Replace with particle system or remove completely + +#pragma once + +#include + +#include "core/types.hpp" + +// Funcions matemàtiques geomètriques pures (sense dependències d'estat) + +// Càlcul de la magnitud d'un vector +float modul(const Punt& p); + +// Diferència entre dos punts (vector origen - destí) +void diferencia(const Punt& o, const Punt& d, Punt& p); + +// Distància entre dos punts +int distancia(const Punt& o, const Punt& d); + +// Càlcul de l'angle d'un punt +float angle_punt(const Punt& p); + +// Creació de polígons regulars +void crear_poligon_regular(Poligon& pol, uint8_t n, float r); diff --git a/source/core/rendering/sdl_manager.cpp b/source/core/rendering/sdl_manager.cpp index 22477fe..e3cea8d 100644 --- a/source/core/rendering/sdl_manager.cpp +++ b/source/core/rendering/sdl_manager.cpp @@ -8,6 +8,7 @@ #include #include "core/defaults.hpp" +#include "core/input/mouse.hpp" #include "core/rendering/coordinate_transform.hpp" #include "core/rendering/line_renderer.hpp" #include "game/options.hpp" @@ -147,6 +148,9 @@ SDLManager::SDLManager(int width, int height, bool fullscreen) std::cout << " [FULLSCREEN]"; } std::cout << std::endl; + + // Inicialitzar mòdul Mouse amb l'estat actual de fullscreen + Mouse::setForceHidden(is_fullscreen_); } SDLManager::~SDLManager() { @@ -356,6 +360,10 @@ void SDLManager::toggleFullscreen() { } Options::window.fullscreen = is_fullscreen_; + + // Notificar al mòdul Mouse: Fullscreen requereix ocultació permanent del cursor. + // Quan es surt de fullscreen, restaurar el comportament normal d'auto-ocultació. + Mouse::setForceHidden(is_fullscreen_); } bool SDLManager::handleWindowEvent(const SDL_Event& event) { diff --git a/source/core/rendering/shape_renderer.cpp b/source/core/rendering/shape_renderer.cpp new file mode 100644 index 0000000..6e86c58 --- /dev/null +++ b/source/core/rendering/shape_renderer.cpp @@ -0,0 +1,80 @@ +// shape_renderer.cpp - Implementació del renderitzat de formes +// © 2025 Port a C++20 amb SDL3 + +#include "core/rendering/shape_renderer.hpp" + +#include + +#include "core/defaults.hpp" +#include "core/rendering/line_renderer.hpp" + +namespace Rendering { + +// Helper: transformar un punt amb rotació, escala i trasllació +static Punt transform_point(const Punt& point, const Punt& shape_centre, const Punt& posicio, float angle, float escala) { + // 1. Centrar el punt respecte al centre de la forma + float centered_x = point.x - shape_centre.x; + float centered_y = point.y - shape_centre.y; + + // 2. Aplicar escala al punt centrat + float scaled_x = centered_x * escala; + float scaled_y = centered_y * escala; + + // 3. Aplicar rotació + // IMPORTANT: En el sistema original, angle=0 apunta AMUNT (no dreta) + // Per això usem (angle - PI/2) per compensar + // Però aquí angle ja ve en el sistema correcte del joc + float cos_a = std::cos(angle); + float sin_a = std::sin(angle); + + float rotated_x = scaled_x * cos_a - scaled_y * sin_a; + float rotated_y = scaled_x * sin_a + scaled_y * cos_a; + + // 4. Aplicar trasllació a posició mundial + return {rotated_x + posicio.x, rotated_y + posicio.y}; +} + +void render_shape(SDL_Renderer* renderer, + const std::shared_ptr& shape, + const Punt& posicio, + float angle, + float escala, + bool dibuixar, + float progress, + float brightness) { + // Verificar que la forma és vàlida + if (!shape || !shape->es_valida()) { + return; + } + + // Si progress < 1.0, no dibuixar (tot o res) + if (progress < 1.0f) { + return; + } + + // Obtenir el centre de la forma per a transformacions + const Punt& shape_centre = shape->get_centre(); + + // Iterar sobre totes les primitives + for (const auto& primitive : shape->get_primitives()) { + if (primitive.type == Graphics::PrimitiveType::POLYLINE) { + // POLYLINE: connectar punts consecutius + for (size_t i = 0; i < primitive.points.size() - 1; i++) { + Punt p1 = transform_point(primitive.points[i], shape_centre, posicio, angle, escala); + Punt p2 = transform_point(primitive.points[i + 1], shape_centre, posicio, angle, escala); + + linea(renderer, static_cast(p1.x), static_cast(p1.y), static_cast(p2.x), static_cast(p2.y), dibuixar, brightness); + } + } else { // PrimitiveType::LINE + // LINE: exactament 2 punts + if (primitive.points.size() >= 2) { + Punt p1 = transform_point(primitive.points[0], shape_centre, posicio, angle, escala); + Punt p2 = transform_point(primitive.points[1], shape_centre, posicio, angle, escala); + + linea(renderer, static_cast(p1.x), static_cast(p1.y), static_cast(p2.x), static_cast(p2.y), dibuixar, brightness); + } + } + } +} + +} // namespace Rendering diff --git a/source/core/rendering/shape_renderer.hpp b/source/core/rendering/shape_renderer.hpp new file mode 100644 index 0000000..8bb6ffe --- /dev/null +++ b/source/core/rendering/shape_renderer.hpp @@ -0,0 +1,33 @@ +// shape_renderer.hpp - Renderitzat de formes vectorials +// © 2025 Port a C++20 amb SDL3 + +#pragma once + +#include + +#include + +#include "core/graphics/shape.hpp" +#include "core/types.hpp" + +namespace Rendering { + +// Renderitzar forma amb transformacions +// - renderer: SDL renderer +// - shape: forma vectorial a dibuixar +// - posicio: posició del centre en coordenades mundials +// - angle: rotació en radians (0 = amunt, sentit horari) +// - escala: factor d'escala (1.0 = mida original) +// - dibuixar: flag per dibuixar (false per col·lisions futures) +// - progress: progrés de l'animació (0.0-1.0, default 1.0 = tot visible) +// - brightness: factor de brillantor (0.0-1.0, default 1.0 = màxima brillantor) +void render_shape(SDL_Renderer* renderer, + const std::shared_ptr& shape, + const Punt& posicio, + float angle, + float escala = 1.0f, + bool dibuixar = true, + float progress = 1.0f, + float brightness = 1.0f); + +} // namespace Rendering diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp new file mode 100644 index 0000000..059cd06 --- /dev/null +++ b/source/core/system/director.cpp @@ -0,0 +1,191 @@ +#include "director.hpp" + +#include +#include + +#include +#include +#include + +#include "../../game/escenes/escena_joc.hpp" +#include "../../game/escenes/escena_logo.hpp" +#include "../../game/escenes/escena_titol.hpp" +#include "../../game/options.hpp" +#include "../audio/audio.hpp" +#include "../defaults.hpp" +#include "../rendering/sdl_manager.hpp" +#include "gestor_escenes.hpp" +#include "project.h" + +#ifndef _WIN32 +#include +#include +#endif + +// Constructor +Director::Director(std::vector const& args) { + std::cout << "Orni Attack - Inici\n"; + + // Inicialitzar opcions amb valors per defecte + Options::init(); + + // Comprovar arguments del programa + executable_path_ = checkProgramArguments(args); + + // Crear carpetes del sistema + createSystemFolder("jailgames"); + createSystemFolder(std::string("jailgames/") + Project::NAME); + + // Establir ruta del fitxer de configuració + Options::setConfigFile(system_folder_ + "/config.yaml"); + + // Carregar o crear configuració + Options::loadFromFile(); + + if (Options::console) { + std::cout << "Configuració carregada\n"; + std::cout << " Finestra: " << Options::window.width << "×" + << Options::window.height << '\n'; + std::cout << " Física: rotation=" << Options::physics.rotation_speed + << " rad/s\n"; + } + + std::cout << '\n'; +} + +Director::~Director() { + // Guardar opcions + Options::saveToFile(); + + // Cleanup audio + Audio::destroy(); + + // Cleanup SDL + SDL_Quit(); + + std::cout << "\nAdéu!\n"; +} + +// Comprovar arguments del programa +auto Director::checkProgramArguments(std::vector const& args) + -> std::string { + for (std::size_t i = 1; i < args.size(); ++i) { + const std::string& argument = args[i]; + + if (argument == "--console") { + Options::console = true; + std::cout << "Mode consola activat\n"; + } else if (argument == "--reset-config") { + Options::init(); + Options::saveToFile(); + std::cout << "Configuració restablida als valors per defecte\n"; + } + } + + return args[0]; // Retornar ruta de l'executable +} + +// Crear carpeta del sistema (específic per plataforma) +void Director::createSystemFolder(const std::string& folder) { +#ifdef _WIN32 + system_folder_ = std::string(getenv("APPDATA")) + "/" + folder; +#elif __APPLE__ + struct passwd* pw = getpwuid(getuid()); + const char* homedir = pw->pw_dir; + system_folder_ = + std::string(homedir) + "/Library/Application Support/" + folder; +#elif __linux__ + struct passwd* pw = getpwuid(getuid()); + const char* homedir = pw->pw_dir; + system_folder_ = std::string(homedir) + "/.config/" + folder; + + // CRÍTIC: Crear ~/.config si no existeix + { + std::string config_base_folder = std::string(homedir) + "/.config"; + int ret = mkdir(config_base_folder.c_str(), S_IRWXU); + if (ret == -1 && errno != EEXIST) { + printf("ERROR: No es pot crear la carpeta ~/.config\n"); + exit(EXIT_FAILURE); + } + } +#endif + + // Comprovar si la carpeta existeix + struct stat st = {.st_dev = 0}; + if (stat(system_folder_.c_str(), &st) == -1) { + errno = 0; + +#ifdef _WIN32 + int ret = mkdir(system_folder_.c_str()); +#else + int ret = mkdir(system_folder_.c_str(), S_IRWXU); +#endif + + if (ret == -1) { + switch (errno) { + case EACCES: + printf("ERROR: Permisos denegats creant %s\n", system_folder_.c_str()); + exit(EXIT_FAILURE); + + case EEXIST: + // La carpeta ja existeix (race condition), continuar + break; + + case ENAMETOOLONG: + printf("ERROR: Ruta massa llarga: %s\n", system_folder_.c_str()); + exit(EXIT_FAILURE); + + default: + perror("mkdir"); + exit(EXIT_FAILURE); + } + } + } + + if (Options::console) { + std::cout << "Carpeta del sistema: " << system_folder_ << '\n'; + } +} + +// Bucle principal del joc +auto Director::run() -> int { + // Calculate initial size from saved zoom_factor + int initial_width = static_cast(std::round( + Defaults::Window::WIDTH * Options::window.zoom_factor)); + int initial_height = static_cast(std::round( + Defaults::Window::HEIGHT * Options::window.zoom_factor)); + + // Crear gestor SDL amb configuració de Options + SDLManager sdl(initial_width, initial_height, Options::window.fullscreen); + + // Inicialitzar sistema d'audio + Audio::init(); + + // Bucle principal de gestió d'escenes + while (GestorEscenes::actual != GestorEscenes::Escena::EIXIR) { + switch (GestorEscenes::actual) { + case GestorEscenes::Escena::LOGO: { + EscenaLogo logo(sdl); + logo.executar(); + break; + } + + case GestorEscenes::Escena::TITOL: { + EscenaTitol titol(sdl); + titol.executar(); + break; + } + + case GestorEscenes::Escena::JOC: { + EscenaJoc joc(sdl); + joc.executar(); + break; + } + + default: + break; + } + } + + return 0; +} diff --git a/source/core/system/director.hpp b/source/core/system/director.hpp new file mode 100644 index 0000000..b1ce50f --- /dev/null +++ b/source/core/system/director.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +class Director { + public: + explicit Director(std::vector const& args); + ~Director(); + + auto run() -> int; // Main game loop + + private: + std::string executable_path_; + std::string system_folder_; + + static auto checkProgramArguments(std::vector const& args) + -> std::string; + void createSystemFolder(const std::string& folder); +}; diff --git a/source/core/system/gestor_escenes.hpp b/source/core/system/gestor_escenes.hpp new file mode 100644 index 0000000..63e6ec4 --- /dev/null +++ b/source/core/system/gestor_escenes.hpp @@ -0,0 +1,17 @@ +// gestor_escenes.hpp - Sistema de gestió d'escenes +// Basat en el patró del projecte "pollo" +// © 2025 Port a C++20 + +#pragma once + +namespace GestorEscenes { +enum class Escena { + LOGO, // Pantalla d'inici (2 segons negre) + TITOL, // Pantalla de títol amb menú + JOC, // Joc principal + EIXIR // Sortir del programa +}; + +// Variable global inline per gestionar l'escena actual +inline Escena actual = Escena::LOGO; +} // namespace GestorEscenes diff --git a/source/core/system/global_events.cpp b/source/core/system/global_events.cpp new file mode 100644 index 0000000..2dd0e57 --- /dev/null +++ b/source/core/system/global_events.cpp @@ -0,0 +1,48 @@ +// global_events.cpp - Implementació dels events globals +// © 2025 Port a C++20 + +#include "global_events.hpp" + +#include "../rendering/sdl_manager.hpp" +#include "gestor_escenes.hpp" +#include "core/input/mouse.hpp" + +namespace GlobalEvents { + +bool handle(const SDL_Event& event, SDLManager& sdl) { + // Tecles globals de finestra (F1/F2/F3) + if (event.type == SDL_EVENT_KEY_DOWN) { + switch (event.key.key) { + case SDLK_F1: + sdl.decreaseWindowSize(); + return true; + case SDLK_F2: + sdl.increaseWindowSize(); + return true; + case SDLK_F3: + sdl.toggleFullscreen(); + return true; + case SDLK_F4: + sdl.toggleVSync(); + return true; + case SDLK_ESCAPE: + GestorEscenes::actual = GestorEscenes::Escena::EIXIR; + return true; + default: + break; + } + } + + // Tancar finestra + if (event.type == SDL_EVENT_QUIT) { + GestorEscenes::actual = GestorEscenes::Escena::EIXIR; + return true; + } + + // Gestió del ratolí (auto-ocultar) + Mouse::handleEvent(event); + + return false; // Event no processat +} + +} // namespace GlobalEvents diff --git a/source/core/system/global_events.hpp b/source/core/system/global_events.hpp new file mode 100644 index 0000000..d972090 --- /dev/null +++ b/source/core/system/global_events.hpp @@ -0,0 +1,16 @@ +// global_events.hpp - Events globals del joc +// Basat en el patró del projecte "pollo" +// © 2025 Port a C++20 + +#pragma once + +#include + +// Forward declaration +class SDLManager; + +namespace GlobalEvents { +// Processa events globals (F1/F2/F3/ESC/QUIT) +// Retorna true si l'event ha estat processat i no cal seguir processant-lo +bool handle(const SDL_Event& event, SDLManager& sdl); +} // namespace GlobalEvents diff --git a/source/core/types.hpp b/source/core/types.hpp new file mode 100644 index 0000000..1c75a3e --- /dev/null +++ b/source/core/types.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include "core/defaults.hpp" + +// Punt polar (coordenades polars) +struct IPunt { + float r; // Radi + float angle; // Angle en radians +}; + +// Punt cartesià +struct Punt { + float x, y; +}; + +// ============================================================================== +// DEPRECATED: Legacy types (replaced by Shape system) +// ============================================================================== +// These types are kept temporarily for chatarra_cosmica_ (Phase 10: explosions) +// TODO Phase 10: Replace with particle system or remove completely + +// Nau (triangle) - DEPRECATED: Now using Shape system (ship.shp) +struct Triangle { + IPunt p1, p2, p3; + Punt centre; + float angle; + float velocitat; +}; + +// Polígon (enemics i bales) - DEPRECATED: Now using Shape system (.shp files) +struct Poligon { + std::array ipuntx; + Punt centre; + float angle; + float velocitat; + uint8_t n; + float drotacio; + float rotacio; + bool esta; +}; diff --git a/source/game/escenes/escena_joc.cpp b/source/game/escenes/escena_joc.cpp index ea9934e..d5f3357 100644 --- a/source/game/escenes/escena_joc.cpp +++ b/source/game/escenes/escena_joc.cpp @@ -11,6 +11,7 @@ #include #include "../../core/audio/audio.hpp" +#include "../../core/input/mouse.hpp" #include "../../core/rendering/line_renderer.hpp" #include "../../core/system/gestor_escenes.hpp" #include "../../core/system/global_events.hpp" @@ -55,6 +56,9 @@ void EscenaJoc::executar() { // Actualitzar comptador de FPS sdl_.updateFPS(delta_time); + // Actualitzar visibilitat del cursor (auto-ocultar) + Mouse::updateCursorVisibility(); + // Processar events SDL while (SDL_PollEvent(&event)) { // Manejo de finestra diff --git a/source/game/escenes/escena_logo.cpp b/source/game/escenes/escena_logo.cpp index 79c06af..8640b5d 100644 --- a/source/game/escenes/escena_logo.cpp +++ b/source/game/escenes/escena_logo.cpp @@ -11,6 +11,7 @@ #include "core/audio/audio.hpp" #include "core/graphics/shape_loader.hpp" +#include "core/input/mouse.hpp" #include "core/rendering/shape_renderer.hpp" #include "core/system/gestor_escenes.hpp" #include "core/system/global_events.hpp" @@ -67,6 +68,9 @@ void EscenaLogo::executar() { // Actualitzar comptador de FPS sdl_.updateFPS(delta_time); + // Actualitzar visibilitat del cursor (auto-ocultar) + Mouse::updateCursorVisibility(); + // Processar events SDL while (SDL_PollEvent(&event)) { // Manejo de finestra diff --git a/source/game/escenes/escena_titol.cpp b/source/game/escenes/escena_titol.cpp index cb7c101..578be7d 100644 --- a/source/game/escenes/escena_titol.cpp +++ b/source/game/escenes/escena_titol.cpp @@ -7,6 +7,7 @@ #include #include "../../core/audio/audio.hpp" +#include "../../core/input/mouse.hpp" #include "../../core/system/gestor_escenes.hpp" #include "../../core/system/global_events.hpp" #include "project.h" @@ -54,6 +55,9 @@ void EscenaTitol::executar() { // Actualitzar comptador de FPS sdl_.updateFPS(delta_time); + // Actualitzar visibilitat del cursor (auto-ocultar) + Mouse::updateCursorVisibility(); + // Processar events SDL while (SDL_PollEvent(&event)) { // Manejo de finestra