reestructuració

This commit is contained in:
2026-04-14 13:26:22 +02:00
parent 4ac34b8583
commit 4429cd92c1
143 changed files with 0 additions and 0 deletions

162
source/core/audio/audio.cpp Normal file
View File

@@ -0,0 +1,162 @@
#include "audio.hpp"
#include <SDL3/SDL.h> // Para SDL_LogInfo, SDL_LogCategory, SDL_G...
#include <algorithm> // Para clamp
#include <iostream> // 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"
// stb_vorbis.h filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem
// perquè xocarien amb noms de paràmetres de plantilla en json.hpp i altres.
#undef L
#undef C
#undef R
#undef PLAYBACK_MONO
#undef PLAYBACK_LEFT
#undef PLAYBACK_RIGHT
// clang-format on
#include "external/jail_audio.hpp" // Para JA_FadeOutMusic, JA_Init, JA_PauseM...
#include "options.hpp" // Para AudioOptions, audio, MusicOptions
#include "resource.hpp" // Para Resource
// 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) {
music_.name = name;
music_.loop = (loop != 0);
if (music_enabled_ && music_.state != MusicState::PLAYING) {
JA_PlayMusic(Resource::get()->getMusic(name), 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
void Audio::playSound(const std::string& name, Group group) const {
if (sound_enabled_) {
JA_PlaySound(Resource::get()->getSound(name), 0, static_cast<int>(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(int sound_volume, Group group) const {
if (sound_enabled_) {
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = (sound_volume / 100.0F) * (Options::audio.volume / 100.0F);
JA_SetSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
}
}
// Establece el volumen de la música
void Audio::setMusicVolume(int music_volume) const {
if (music_enabled_) {
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = (music_volume / 100.0F) * (Options::audio.volume / 100.0F);
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)) {
std::cout << "SDL_AUDIO could not initialize! SDL Error: " << SDL_GetError() << '\n';
} else {
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
enable(Options::audio.enabled);
}
}

111
source/core/audio/audio.hpp Normal file
View File

@@ -0,0 +1,111 @@
#pragma once
#include <string> // Para string
#include <utility> // 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 int MAX_VOLUME = 100; // Volumen máximo
static constexpr int MIN_VOLUME = 0; // Volumen mínimo
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
// --- Métodos de 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
// --- Método principal ---
static void update();
// --- 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
void stopAllSounds() const; // Detener todos los sonidos
// --- 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
// --- Control de Volumen ---
void setSoundVolume(int volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
void setMusicVolume(int volume) const; // Ajustar volumen de música
// --- Getters para debug ---
[[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; // Consulta directamente a jailaudio
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
private:
// --- Estructuras privadas ---
struct Music {
MusicState state; // Estado actual de la música (reproduciendo, detenido, en pausa)
std::string name; // Última pista de música reproducida
bool loop; // Indica si la última pista de música se debe reproducir en bucle
// Constructor para inicializar la música con valores predeterminados
Music()
: state(MusicState::STOPPED),
loop(false) {}
// Constructor para inicializar con valores específicos
Music(MusicState init_state, std::string init_name, bool init_loop)
: state(init_state),
name(std::move(init_name)),
loop(init_loop) {}
};
// --- Variables de estado ---
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
// --- Métodos internos ---
void initSDLAudio(); // Inicializa SDL Audio
// --- Constructores y destructor privados (singleton) ---
Audio(); // Constructor privado
~Audio(); // Destructor privado
// --- Instancia singleton ---
static Audio* instance; // Instancia única de Audio
};

View File

@@ -0,0 +1,557 @@
#pragma once
// --- Includes ---
#include <SDL3/SDL.h>
#include <stdint.h> // Para uint32_t, uint8_t
#include <stdio.h> // Para NULL, fseek, printf, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
#include <stdlib.h> // Para free, malloc
#include <string.h> // Para strcpy, strlen
#include <iostream> // Para std::cout
#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};
// OGG comprimit en memòria. Propietat nostra; es copia des del fitxer una
// sola vegada en JA_LoadMusic i es descomprimix en chunks per streaming.
Uint8* ogg_data{nullptr};
Uint32 ogg_length{0};
stb_vorbis* vorbis{nullptr}; // Handle del decoder, viu tot el cicle del JA_Music_t
char* filename{nullptr};
int times{0}; // Loops restants (-1 = infinit, 0 = un sol play)
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);
// --- Music streaming internals ---
// Bytes-per-sample per canal (sempre s16)
static constexpr int JA_MUSIC_BYTES_PER_SAMPLE = 2;
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
static constexpr int JA_MUSIC_CHUNK_SHORTS = 8192;
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
static constexpr float JA_MUSIC_LOW_WATER_SECONDS = 0.5f;
// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples
// decodificats per canal (0 = EOF de l'stream vorbis).
inline int JA_FeedMusicChunk(JA_Music_t* music) {
if (!music || !music->vorbis || !music->stream) return 0;
short chunk[JA_MUSIC_CHUNK_SHORTS];
const int channels = music->spec.channels;
const int samples_per_channel = stb_vorbis_get_samples_short_interleaved(
music->vorbis,
channels,
chunk,
JA_MUSIC_CHUNK_SHORTS);
if (samples_per_channel <= 0) return 0;
const int bytes = samples_per_channel * channels * JA_MUSIC_BYTES_PER_SAMPLE;
SDL_PutAudioStreamData(music->stream, chunk, bytes);
return samples_per_channel;
}
// Reompli l'stream fins que tinga ≥ JA_MUSIC_LOW_WATER_SECONDS bufferats.
// En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar.
inline void JA_PumpMusic(JA_Music_t* music) {
if (!music || !music->vorbis || !music->stream) return;
const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE;
const int low_water_bytes = static_cast<int>(JA_MUSIC_LOW_WATER_SECONDS * static_cast<float>(bytes_per_second));
while (SDL_GetAudioStreamAvailable(music->stream) < low_water_bytes) {
const int decoded = JA_FeedMusicChunk(music);
if (decoded > 0) continue;
// EOF: si queden loops, rebobinar; si no, tallar i deixar drenar.
if (music->times != 0) {
stb_vorbis_seek_start(music->vorbis);
if (music->times > 0) music->times--;
} else {
break;
}
}
}
// --- 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));
}
}
// Streaming: rellenem l'stream fins al low-water-mark i parem si el
// vorbis s'ha esgotat i no queden loops.
JA_PumpMusic(current_music);
if (current_music->times == 0 && 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) std::cout << "Failed to initialize SDL audio!" << '\n';
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) {
if (!buffer || length == 0) return nullptr;
// Còpia del OGG comprimit: stb_vorbis llig de forma persistent aquesta
// memòria mentre el handle estiga viu, així que hem de posseir-la nosaltres.
Uint8* ogg_copy = static_cast<Uint8*>(SDL_malloc(length));
if (!ogg_copy) return nullptr;
SDL_memcpy(ogg_copy, buffer, length);
int error = 0;
stb_vorbis* vorbis = stb_vorbis_open_memory(ogg_copy, static_cast<int>(length), &error, nullptr);
if (!vorbis) {
SDL_free(ogg_copy);
std::cout << "JA_LoadMusic: stb_vorbis_open_memory failed (error " << error << ")" << '\n';
return nullptr;
}
auto* music = new JA_Music_t();
music->ogg_data = ogg_copy;
music->ogg_length = length;
music->vorbis = vorbis;
const stb_vorbis_info info = stb_vorbis_get_info(vorbis);
music->spec.channels = info.channels;
music->spec.freq = static_cast<int>(info.sample_rate);
music->spec.format = SDL_AUDIO_S16;
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<Uint8*>(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<char*>(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 || !music->vorbis) return;
JA_StopMusic();
current_music = music;
current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop;
// Rebobinem l'stream de vorbis al principi. Cobreix tant play-per-primera-
// vegada com replays/canvis de track que tornen a la mateixa pista.
stb_vorbis_seek_start(current_music->vorbis);
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!current_music->stream) {
std::cout << "Failed to create audio stream!" << '\n';
current_music->state = JA_MUSIC_STOPPED;
return;
}
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
// Pre-cargem el buffer abans de bindejar per evitar un underrun inicial.
JA_PumpMusic(current_music);
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->state = JA_MUSIC_STOPPED;
if (current_music->stream) {
SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr;
}
// Deixem el handle de vorbis viu — es tanca en JA_DeleteMusic.
// Rebobinem perquè un futur JA_PlayMusic comence des del principi.
if (current_music->vorbis) {
stb_vorbis_seek_start(current_music->vorbis);
}
// No liberem filename aquí; es fa 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;
}
if (music->stream) SDL_DestroyAudioStream(music->stream);
if (music->vorbis) stb_vorbis_close(music->vorbis);
SDL_free(music->ogg_data);
free(music->filename); // filename es 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*/) {
// No implementat amb el backend de streaming. Mai va arribar a usar-se
// en el codi existent, així que es manté com a stub.
}
inline float JA_GetMusicPosition() {
// Veure nota a JA_SetMusicPosition.
return 0.0f;
}
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)) {
std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n';
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)) {
std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n';
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) {
std::cout << "Failed to create audio stream for sound!" << '\n';
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;
}

View File

@@ -0,0 +1,267 @@
#include "define_buttons.hpp"
#include <algorithm> // Para __all_of_fn, all_of
#include <memory> // Para unique_ptr, allocator, shared_ptr, operator==, make_unique
#include "input.hpp" // Para Input
#include "input_types.hpp" // Para InputAction
#include "lang.hpp" // Para getText
#include "options.hpp" // Para Gamepad
#include "param.hpp" // Para Param, param, ParamGame, ParamServiceMenu
#include "resource.hpp" // Para Resource
#include "ui/window_message.hpp" // Para WindowMessage
#include "utils.hpp" // Para Zone
DefineButtons::DefineButtons()
: input_(Input::get()) {
clearButtons();
auto gamepads = input_->getGamepads();
for (const auto& gamepad : gamepads) {
controller_names_.emplace_back(Input::getControllerName(gamepad));
}
// Crear la ventana de mensaje
WindowMessage::Config config(param.service_menu.window_message);
auto text_renderer = Resource::get()->getText("04b_25_flat");
window_message_ = std::make_unique<WindowMessage>(
text_renderer,
Lang::getText("[DEFINE_BUTTONS] TITLE"),
config);
window_message_->setPosition(param.game.game_area.center_x, param.game.game_area.center_y, WindowMessage::PositionMode::CENTERED);
}
void DefineButtons::render() {
if (enabled_ && window_message_) {
window_message_->render();
}
}
void DefineButtons::update(float delta_time) {
if (!enabled_) {
return;
}
// Actualizar la ventana siempre
if (window_message_) {
window_message_->update(delta_time);
}
// Manejar la secuencia de cierre si ya terminamos
if (finished_ && message_shown_) {
message_timer_ += delta_time;
// Después del delay, iniciar animación de cierre (solo una vez)
if (message_timer_ >= MESSAGE_DISPLAY_DURATION_S && !closing_) {
if (window_message_) {
window_message_->hide(); // Iniciar animación de cierre
}
closing_ = true;
}
}
}
void DefineButtons::handleEvents(const SDL_Event& event) {
if (enabled_) {
switch (event.type) {
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
doControllerButtonDown(event.gbutton);
break;
case SDL_EVENT_GAMEPAD_BUTTON_UP:
checkEnd();
break;
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
doControllerAxisMotion(event.gaxis);
break;
default:
break;
}
}
}
auto DefineButtons::enable(Options::Gamepad* options_gamepad) -> bool {
if (options_gamepad != nullptr) {
options_gamepad_ = options_gamepad;
enabled_ = true;
finished_ = false;
index_button_ = 0;
message_shown_ = false;
closing_ = false;
l2_was_pressed_ = false;
r2_was_pressed_ = false;
clearButtons();
updateWindowMessage();
if (window_message_) {
window_message_->autoSize();
window_message_->show();
}
return true;
}
return false;
}
void DefineButtons::disable() {
enabled_ = false;
finished_ = false;
message_shown_ = false;
closing_ = false;
l2_was_pressed_ = false;
r2_was_pressed_ = false;
if (window_message_) {
window_message_->hide();
}
}
void DefineButtons::doControllerButtonDown(const SDL_GamepadButtonEvent& event) {
auto gamepad = input_->getGamepad(event.which);
if (!gamepad || gamepad != options_gamepad_->instance) {
return;
}
const auto BUTTON = static_cast<SDL_GamepadButton>(event.button);
if (checkButtonNotInUse(BUTTON)) {
buttons_.at(index_button_).button = static_cast<int>(BUTTON);
incIndexButton();
updateWindowMessage();
}
}
void DefineButtons::doControllerAxisMotion(const SDL_GamepadAxisEvent& event) {
auto gamepad = input_->getGamepad(event.which);
if (!gamepad || gamepad != options_gamepad_->instance) {
return;
}
// Solo manejamos L2 y R2 como botones con lógica de transición
if (event.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) {
bool l2_is_pressed_now = event.value > 16384;
// Solo actuar en la transición de no presionado a presionado
if (l2_is_pressed_now && !l2_was_pressed_) {
const auto TRIGGER_BUTTON = Input::TRIGGER_L2_AS_BUTTON;
if (checkTriggerNotInUse(TRIGGER_BUTTON)) {
buttons_.at(index_button_).button = TRIGGER_BUTTON;
incIndexButton();
updateWindowMessage();
}
}
// Detectar liberación del trigger para llamar checkEnd()
if (!l2_is_pressed_now && l2_was_pressed_) {
checkEnd();
}
l2_was_pressed_ = l2_is_pressed_now;
} else if (event.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
bool r2_is_pressed_now = event.value > 16384;
// Solo actuar en la transición de no presionado a presionado
if (r2_is_pressed_now && !r2_was_pressed_) {
const auto TRIGGER_BUTTON = Input::TRIGGER_R2_AS_BUTTON;
if (checkTriggerNotInUse(TRIGGER_BUTTON)) {
buttons_.at(index_button_).button = TRIGGER_BUTTON;
incIndexButton();
updateWindowMessage();
}
}
// Detectar liberación del trigger para llamar checkEnd()
if (!r2_is_pressed_now && r2_was_pressed_) {
checkEnd();
}
r2_was_pressed_ = r2_is_pressed_now;
}
}
void DefineButtons::bindButtons(Options::Gamepad* options_gamepad) {
for (const auto& button : buttons_) {
Input::bindGameControllerButton(options_gamepad->instance, button.action, static_cast<SDL_GamepadButton>(button.button));
}
Input::bindGameControllerButton(options_gamepad->instance, Input::Action::SM_SELECT, Input::Action::FIRE_LEFT);
Input::bindGameControllerButton(options_gamepad->instance, Input::Action::SM_BACK, Input::Action::FIRE_CENTER);
}
void DefineButtons::incIndexButton() {
if (index_button_ < buttons_.size() - 1) {
++index_button_;
} else {
finished_ = true;
}
}
auto DefineButtons::checkButtonNotInUse(SDL_GamepadButton button) -> bool {
return std::ranges::all_of(buttons_, [button](const auto& b) -> auto {
return b.button != button;
});
}
auto DefineButtons::checkTriggerNotInUse(int trigger_button) -> bool {
return std::ranges::all_of(buttons_, [trigger_button](const auto& b) -> auto {
return b.button != trigger_button;
});
}
void DefineButtons::clearButtons() {
buttons_.clear();
buttons_.emplace_back(Lang::getText("[DEFINE_BUTTONS] FIRE_LEFT"), Input::Action::FIRE_LEFT, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID));
buttons_.emplace_back(Lang::getText("[DEFINE_BUTTONS] FIRE_UP"), Input::Action::FIRE_CENTER, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID));
buttons_.emplace_back(Lang::getText("[DEFINE_BUTTONS] FIRE_RIGHT"), Input::Action::FIRE_RIGHT, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID));
buttons_.emplace_back(Lang::getText("[DEFINE_BUTTONS] START"), Input::Action::START, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID));
buttons_.emplace_back(Lang::getText("[DEFINE_BUTTONS] SERVICE_MENU"), Input::Action::SERVICE, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID));
}
void DefineButtons::checkEnd() {
if (finished_ && !message_shown_) {
bindButtons(options_gamepad_);
input_->saveGamepadConfigFromGamepad(options_gamepad_->instance);
input_->resetInputStates();
// Mostrar mensaje de finalización
if (window_message_) {
window_message_->clearTexts();
window_message_->addText(Lang::getText("[DEFINE_BUTTONS] CONFIGURATION_COMPLETE"));
}
// Solo marcar que ya mostramos el mensaje
message_shown_ = true;
message_timer_ = 0.0F;
}
}
auto DefineButtons::isReadyToClose() const -> bool {
// Solo está listo para cerrar si:
// 1. Terminó
// 2. Ya mostró el mensaje
// 3. Está cerrando
// 4. La ventana ya no está visible (animación terminada)
return finished_ && message_shown_ && closing_ &&
(!window_message_ || !window_message_->isVisible());
}
void DefineButtons::updateWindowMessage() {
if (!window_message_ || (options_gamepad_ == nullptr)) {
return;
}
// Configurar título
std::string title = Lang::getText("[DEFINE_BUTTONS] CONFIGURING") + ": " + options_gamepad_->name;
window_message_->setTitle(title);
// Limpiar textos anteriores
window_message_->clearTexts();
if (index_button_ < buttons_.size()) {
// Instrucción actual
std::string instruction = Lang::getText("[DEFINE_BUTTONS] PRESS_BUTTON_FOR") + ":";
window_message_->addText(instruction);
window_message_->addText(buttons_.at(index_button_).label);
}
}

View File

@@ -0,0 +1,80 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstddef>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "input.hpp"
#include "ui/window_message.hpp"
namespace Options {
struct Gamepad;
}
// --- Clase DefineButtons: configuración de botones de gamepad ---
class DefineButtons {
public:
// --- Estructuras ---
struct Button {
std::string label;
Input::Action action;
int button;
Button(std::string label, Input::Action action, int button)
: label(std::move(label)),
action(action),
button(button) {}
};
// --- Constructor y destructor ---
DefineButtons();
~DefineButtons() = default;
// --- Métodos principales ---
void render();
void update(float delta_time);
void handleEvents(const SDL_Event& event);
auto enable(Options::Gamepad* options_gamepad) -> bool;
void disable();
// --- Getters ---
[[nodiscard]] auto isReadyToClose() const -> bool;
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
[[nodiscard]] auto isFinished() const -> bool { return finished_; }
private:
// --- Constantes ---
static constexpr float MESSAGE_DISPLAY_DURATION_S = 2.0F; // Cuánto tiempo mostrar el mensaje en segundos
// --- Objetos y punteros ---
Input* input_ = nullptr; // Entrada del usuario
Options::Gamepad* options_gamepad_ = nullptr; // Opciones del gamepad
std::unique_ptr<WindowMessage> window_message_; // Mensaje de ventana
// --- Variables de estado ---
std::vector<Button> buttons_; // Lista de botones
std::vector<std::string> controller_names_; // Nombres de los controladores
size_t index_button_ = 0; // Índice del botón seleccionado
float message_timer_ = 0.0F; // Timer en segundos para el mensaje
bool enabled_ = false; // Flag para indicar si está activo
bool finished_ = false; // Flag para indicar si ha terminado
bool closing_ = false; // Flag para indicar que está cerrando
bool message_shown_ = false; // Flag para indicar que ya mostró el mensaje
bool l2_was_pressed_ = false; // Estado anterior del trigger L2
bool r2_was_pressed_ = false; // Estado anterior del trigger R2
// --- Métodos internos ---
void incIndexButton();
void doControllerButtonDown(const SDL_GamepadButtonEvent& event);
void doControllerAxisMotion(const SDL_GamepadAxisEvent& event);
void bindButtons(Options::Gamepad* options_gamepad);
auto checkButtonNotInUse(SDL_GamepadButton button) -> bool;
auto checkTriggerNotInUse(int trigger_button) -> bool;
void clearButtons();
void checkEnd();
void updateWindowMessage();
};

View File

@@ -0,0 +1,136 @@
#pragma once
#include <fstream>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include "external/json.hpp"
#include "input_types.hpp" // Solo incluimos los tipos compartidos
// --- Estructuras ---
struct GamepadConfig {
std::string name; // Nombre del dispositivo
std::string path; // Ruta física del dispositivo
std::unordered_map<InputAction, SDL_GamepadButton> bindings; // Asociación acción-botón
GamepadConfig(std::string name, std::string path)
: name(std::move(name)),
path(std::move(path)),
bindings{
{InputAction::FIRE_LEFT, SDL_GAMEPAD_BUTTON_WEST},
{InputAction::FIRE_CENTER, SDL_GAMEPAD_BUTTON_NORTH},
{InputAction::FIRE_RIGHT, SDL_GAMEPAD_BUTTON_EAST},
{InputAction::START, SDL_GAMEPAD_BUTTON_START},
{InputAction::SERVICE, SDL_GAMEPAD_BUTTON_BACK}} {}
// Reasigna un botón a una acción
void rebindAction(InputAction action, SDL_GamepadButton new_button) {
bindings[action] = new_button;
}
};
// --- Tipos ---
using GamepadConfigs = std::vector<GamepadConfig>; // Vector de configuraciones de gamepad
// --- Clase GamepadConfigManager: gestor de configuraciones de gamepad ---
class GamepadConfigManager {
public:
// --- Métodos estáticos ---
static auto writeToJson(const GamepadConfigs& configs, const std::string& filename) -> bool { // Escribir configuraciones a JSON
try {
nlohmann::json j;
j["gamepads"] = nlohmann::json::array();
for (const auto& config : configs) {
nlohmann::json gamepad_json;
gamepad_json["name"] = config.name;
gamepad_json["path"] = config.path;
gamepad_json["bindings"] = nlohmann::json::object();
// Convertir bindings a JSON
for (const auto& [action, button] : config.bindings) {
auto action_it = ACTION_TO_STRING.find(action);
auto button_it = BUTTON_TO_STRING.find(button);
if (action_it != ACTION_TO_STRING.end() && button_it != BUTTON_TO_STRING.end()) {
gamepad_json["bindings"][action_it->second] = button_it->second;
}
}
j["gamepads"].push_back(gamepad_json);
}
// Escribir al archivo
std::ofstream file(filename);
if (!file.is_open()) {
return false; // NOLINT(readability-simplify-boolean-expr)
}
file << j.dump(4); // Formato con indentación de 4 espacios
file.close();
return true;
} catch (const std::exception& e) {
// Log del error si tienes sistema de logging
return false;
}
}
// Leer vector de GamepadConfig desde archivo JSON
static auto readFromJson(GamepadConfigs& configs, const std::string& filename) -> bool {
try {
std::ifstream file(filename);
if (!file.is_open()) {
return false;
}
nlohmann::json j;
file >> j;
file.close();
configs.clear();
if (!j.contains("gamepads") || !j["gamepads"].is_array()) {
return false; // NOLINT(readability-simplify-boolean-expr)
}
for (const auto& gamepad_json : j["gamepads"]) {
if (!gamepad_json.contains("name") || !gamepad_json.contains("bindings")) {
continue; // Saltar configuraciones malformadas
}
// Leer el campo path si existe, si no dejarlo vacío
std::string path = gamepad_json.contains("path") ? gamepad_json["path"].get<std::string>() : "";
GamepadConfig config(gamepad_json["name"], path);
// Limpiar bindings por defecto para cargar los del archivo
config.bindings.clear();
// Cargar bindings desde JSON
for (const auto& [actionStr, buttonStr] : gamepad_json["bindings"].items()) {
auto action_it = STRING_TO_ACTION.find(actionStr);
auto button_it = STRING_TO_BUTTON.find(buttonStr);
if (action_it != STRING_TO_ACTION.end() && button_it != STRING_TO_BUTTON.end()) {
config.bindings[action_it->second] = button_it->second;
}
}
configs.push_back(config);
}
return true;
} catch (const std::exception& e) {
// Log del error si tienes sistema de logging
return false;
}
}
// Método auxiliar para verificar si un archivo existe
static auto fileExists(const std::string& filename) -> bool {
std::ifstream file(filename);
return file.good();
}
};

View File

@@ -0,0 +1,266 @@
#include "global_inputs.hpp"
#include <algorithm> // Para __any_of_fn, any_of
#include <functional> // Para function
#include <iterator> // Para pair
#include <string> // Para basic_string, operator+, allocator, char_traits, string, to_string
#include <utility> // Para pair
#include <vector> // Para vector
#include "audio.hpp" // Para Audio
#include "input.hpp" // Para Input
#include "input_types.hpp" // Para InputAction
#include "lang.hpp" // Para getText, getLangFile, getLangName, getNextLangCode, loadFromFile
#include "options.hpp" // Para Video, video, Settings, settings, Audio, audio, Window, window
#include "screen.hpp" // Para Screen
#include "section.hpp" // Para Name, name, Options, options, AttractMode, attract_mode
#include "ui/notifier.hpp" // Para Notifier
#include "ui/service_menu.hpp" // Para ServiceMenu
#include "utils.hpp" // Para boolToOnOff
namespace GlobalInputs {
// Termina
void quit() {
const std::string CODE = "QUIT";
if (Notifier::get()->checkCode(CODE)) {
// Si la notificación de salir está activa, cambia de sección
Section::name = Section::Name::QUIT;
Section::options = Section::Options::NONE;
} else {
// Si la notificación de salir no está activa, muestra la notificación
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 01"), std::string()}, -1, CODE);
}
}
// Reinicia
void reset() {
const std::string CODE = "RESET";
if (Notifier::get()->checkCode(CODE)) {
Section::name = Section::Name::RESET;
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 15")});
} else {
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 03"), std::string()}, -1, CODE);
}
}
// Activa o desactiva el audio
void toggleAudio() {
Options::audio.enabled = !Options::audio.enabled;
Audio::get()->enable(Options::audio.enabled);
Notifier::get()->show({"Audio " + boolToOnOff(Options::audio.enabled)});
}
// Cambia el modo de escalado entero
void toggleIntegerScale() {
Screen::get()->toggleIntegerScale();
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 12") + " " + boolToOnOff(Options::video.integer_scale)});
}
// Activa / desactiva el vsync
void toggleVSync() {
Screen::get()->toggleVSync();
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 14") + " " + boolToOnOff(Options::video.vsync)});
}
// Activa o desactiva los shaders
void toggleShaders() {
Screen::toggleShaders();
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 13") + " " + boolToOnOff(Options::video.shader.enabled)});
}
// Cambia entre PostFX y CrtPi
void nextShader() {
Screen::nextShader();
const std::string SHADER_NAME = (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) ? "CrtPi" : "PostFX";
Notifier::get()->show({"Shader: " + SHADER_NAME});
}
// Avanza al siguiente preset PostFX o CrtPi según shader activo
void nextPreset() {
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
Screen::nextCrtPiPreset();
const std::string name = Options::crtpi_presets.empty() ? "" : Options::crtpi_presets.at(static_cast<size_t>(Options::video.shader.current_crtpi_preset)).name;
Notifier::get()->show({"CrtPi: " + name});
} else {
Screen::nextPostFXPreset();
const std::string name = Options::postfx_presets.empty() ? "" : Options::postfx_presets.at(static_cast<size_t>(Options::video.shader.current_postfx_preset)).name;
Notifier::get()->show({"PostFX: " + name});
}
}
// Activa o desactiva el supersampling
void toggleSupersampling() {
Screen::toggleSupersampling();
Notifier::get()->show({"SS: " + std::string(Options::video.supersampling.enabled ? "ON" : "OFF")});
}
// Cambia al siguiente idioma
void setNextLang() {
const std::string CODE = "LANG";
const auto NEXT_LANG_CODE = Lang::getNextLangCode(Options::settings.language);
const auto NEXT_LANG_NAME = Lang::getLangName(NEXT_LANG_CODE);
if (Notifier::get()->checkCode(CODE)) {
// Si la notificación de cambiar idioma está activa, cambia de de idioma
Options::settings.language = NEXT_LANG_CODE;
Lang::loadFromFile(Lang::getLangFile(NEXT_LANG_CODE));
Section::name = Section::Name::RESET;
Section::options = Section::Options::RELOAD;
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 05") + NEXT_LANG_NAME});
} else {
// Si la notificación de cambiar idioma no está activa, muestra la notificación
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 04") + NEXT_LANG_NAME, std::string()}, -1, CODE);
}
}
// Cambia el modo de disparo
void toggleFireMode() {
Options::settings.autofire = !Options::settings.autofire;
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 08") + " " + boolToOnOff(Options::settings.autofire)});
}
// Salta una sección del juego
void skipSection() {
switch (Section::name) {
case Section::Name::INTRO:
Audio::get()->stopMusic();
/* Continua en el case de abajo */
case Section::Name::LOGO:
case Section::Name::HI_SCORE_TABLE:
case Section::Name::INSTRUCTIONS: {
Section::name = Section::Name::TITLE;
Section::options = Section::Options::TITLE_1;
Section::attract_mode = Section::AttractMode::TITLE_TO_DEMO;
break;
}
default:
break;
}
}
// Activa el menu de servicio
void toggleServiceMenu() {
ServiceMenu::get()->toggle();
}
// Cambia el modo de pantalla completa
void toggleFullscreen() {
Screen::get()->toggleFullscreen();
const std::string MODE = Options::video.fullscreen ? Lang::getText("[NOTIFICATIONS] 11") : Lang::getText("[NOTIFICATIONS] 10");
Notifier::get()->show({MODE});
}
// Reduce el tamaño de la ventana
void decWindowSize() {
if (Screen::get()->decWindowSize()) {
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 09") + " x" + std::to_string(Options::window.zoom)});
}
}
// Aumenta el tamaño de la ventana
void incWindowSize() {
if (Screen::get()->incWindowSize()) {
Notifier::get()->show({Lang::getText("[NOTIFICATIONS] 09") + " x" + std::to_string(Options::window.zoom)});
}
}
// Comprueba el boton de servicio
auto checkServiceButton() -> bool {
// Teclado
if (Input::get()->checkAction(Input::Action::SERVICE, Input::DO_NOT_ALLOW_REPEAT, Input::CHECK_KEYBOARD)) {
toggleServiceMenu();
return true;
}
// Mandos
if (std::ranges::any_of(Input::get()->getGamepads(), [](const auto& gamepad) -> auto {
return Input::get()->checkAction(Input::Action::SERVICE, Input::DO_NOT_ALLOW_REPEAT, Input::DO_NOT_CHECK_KEYBOARD, gamepad);
})) {
toggleServiceMenu();
return true;
}
return false;
}
// Comprueba las entradas para elementos del sistema
auto checkSystemInputs() -> bool {
using Action = Input::Action;
static const std::vector<std::pair<Action, std::function<void()>>> ACTIONS = {
{Action::WINDOW_FULLSCREEN, toggleFullscreen},
{Action::WINDOW_DEC_SIZE, decWindowSize},
{Action::WINDOW_INC_SIZE, incWindowSize},
{Action::EXIT, quit},
{Action::RESET, reset},
{Action::TOGGLE_AUDIO, toggleAudio},
{Action::TOGGLE_AUTO_FIRE, toggleFireMode},
{Action::CHANGE_LANG, setNextLang},
{Action::TOGGLE_VIDEO_INTEGER_SCALE, toggleIntegerScale},
{Action::TOGGLE_VIDEO_VSYNC, toggleVSync},
#ifdef _DEBUG
{Action::SHOW_INFO, []() -> void { Screen::get()->toggleDebugInfo(); }},
#endif
};
if (std::ranges::any_of(ACTIONS, [](const auto& pair) -> auto {
if (Input::get()->checkAction(pair.first, Input::DO_NOT_ALLOW_REPEAT, Input::CHECK_KEYBOARD)) {
pair.second();
return true;
}
return false;
})) {
return true;
}
if (Input::get()->checkAction(Input::Action::TOGGLE_VIDEO_POSTFX, Input::DO_NOT_ALLOW_REPEAT, Input::CHECK_KEYBOARD)) {
toggleShaders();
return true;
}
if (Input::get()->checkAction(Input::Action::NEXT_SHADER, Input::DO_NOT_ALLOW_REPEAT, Input::CHECK_KEYBOARD)) {
nextShader();
return true;
}
if (Input::get()->checkAction(Input::Action::NEXT_POSTFX_PRESET, Input::DO_NOT_ALLOW_REPEAT, Input::CHECK_KEYBOARD)) {
nextPreset();
return true;
}
if (Input::get()->checkAction(Input::Action::TOGGLE_SUPERSAMPLING, Input::DO_NOT_ALLOW_REPEAT, Input::CHECK_KEYBOARD)) {
toggleSupersampling();
return true;
}
return false;
}
// Comprueba el resto de entradas
auto checkOtherInputs() -> bool {
// Saltar sección
if ((Input::get()->checkAnyButton()) && !ServiceMenu::get()->isEnabled()) {
skipSection();
return true;
}
return false;
}
// Comprueba las entradas del Menu de Servicio
inline auto checkServiceMenuInputs() -> bool {
return ServiceMenu::get()->checkInput();
}
// Comprueba los inputs que se pueden introducir en cualquier sección del juego
auto check() -> bool {
if (checkServiceButton()) {
return true;
}
if (checkServiceMenuInputs()) {
return true;
}
if (checkSystemInputs()) {
return true;
}
if (checkOtherInputs()) {
return true;
}
return false;
}
} // namespace GlobalInputs

View File

@@ -0,0 +1,7 @@
#pragma once
// --- Namespace GlobalInputs: gestiona inputs globales del juego ---
namespace GlobalInputs {
// --- Funciones ---
auto check() -> bool; // Comprueba los inputs que se pueden introducir en cualquier sección del juego
} // namespace GlobalInputs

542
source/core/input/input.cpp Normal file
View File

@@ -0,0 +1,542 @@
#include "input.hpp"
#include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode
#include <iostream> // Para basic_ostream, operator<<, cout, cerr
#include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
#include <ranges> // Para __find_if_fn, find_if
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
#include <utility> // Para pair, move
// Singleton
Input* Input::instance = nullptr;
// Inicializa la instancia única del singleton
void Input::init(const std::string& game_controller_db_path, const std::string& gamepad_configs_file) {
Input::instance = new Input(game_controller_db_path, gamepad_configs_file);
}
// Libera la instancia
void Input::destroy() { delete Input::instance; }
// Obtiene la instancia
auto Input::get() -> Input* { return Input::instance; }
// Constructor
Input::Input(std::string game_controller_db_path, std::string gamepad_configs_file)
: gamepad_mappings_file_(std::move(game_controller_db_path)),
gamepad_configs_file_(std::move(gamepad_configs_file)) {
// Inicializa el subsistema SDL_INIT_GAMEPAD
initSDLGamePad();
}
// Asigna inputs a teclas
void Input::bindKey(Action action, SDL_Scancode code) {
keyboard_.bindings[action].scancode = code;
}
// Asigna inputs a botones del mando
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button) {
if (gamepad != nullptr) {
gamepad->bindings[action].button = button;
}
}
// Asigna inputs a botones del mando
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source) {
if (gamepad != nullptr) {
gamepad->bindings[action_target].button = gamepad->bindings[action_source].button;
}
}
// Comprueba si alguna acción está activa
auto Input::checkAction(Action action, bool repeat, bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool {
bool success_keyboard = false;
bool success_controller = false;
if (check_keyboard) {
if (repeat) { // El usuario quiere saber si está pulsada (estado mantenido)
success_keyboard = keyboard_.bindings[action].is_held;
} else { // El usuario quiere saber si ACABA de ser pulsada (evento de un solo fotograma)
success_keyboard = keyboard_.bindings[action].just_pressed;
}
}
if (gamepad != nullptr) {
success_controller = checkAxisInput(action, gamepad, repeat);
if (!success_controller) {
success_controller = checkTriggerInput(action, gamepad, repeat);
}
if (!success_controller) {
if (repeat) { // El usuario quiere saber si está pulsada (estado mantenido)
success_controller = gamepad->bindings[action].is_held;
} else { // El usuario quiere saber si ACABA de ser pulsada (evento de un solo fotograma)
success_controller = gamepad->bindings[action].just_pressed;
}
}
}
return (success_keyboard || success_controller);
}
// Comprueba si hay almenos una acción activa
auto Input::checkAnyInput(bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool {
// Obtenemos el número total de acciones posibles para iterar sobre ellas.
// --- Comprobación del Teclado ---
if (check_keyboard) {
for (const auto& pair : keyboard_.bindings) {
// Simplemente leemos el estado pre-calculado por Input::update().
// Ya no se llama a SDL_GetKeyboardState ni se modifica el estado '.active'.
if (pair.second.just_pressed) {
return true; // Se encontró una acción recién pulsada.
}
}
}
// --- Comprobación del Mando ---
// Comprobamos si hay mandos y si el índice solicitado es válido.
if (gamepad != nullptr) {
// Iteramos sobre todas las acciones, no sobre el número de mandos.
for (const auto& pair : gamepad->bindings) {
// Leemos el estado pre-calculado para el mando y la acción específicos.
if (pair.second.just_pressed) {
return true; // Se encontró una acción recién pulsada en el mando.
}
}
}
// Si llegamos hasta aquí, no se detectó ninguna nueva pulsación.
return false;
}
// Comprueba si hay algún botón pulsado
auto Input::checkAnyButton(bool repeat) -> bool {
// Solo comprueba los botones definidos previamente
for (auto bi : BUTTON_INPUTS) {
// Comprueba el teclado
if (checkAction(bi, repeat, CHECK_KEYBOARD)) {
return true;
}
// Comprueba los mandos
for (const auto& gamepad : gamepads_) {
if (checkAction(bi, repeat, DO_NOT_CHECK_KEYBOARD, gamepad)) {
return true;
}
}
}
return false;
}
// Comprueba si hay algun mando conectado
auto Input::gameControllerFound() const -> bool { return !gamepads_.empty(); }
// Obten el nombre de un mando de juego
auto Input::getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string {
return gamepad == nullptr ? std::string() : gamepad->name;
}
// Obtiene la lista de nombres de mandos
auto Input::getControllerNames() const -> std::vector<std::string> {
std::vector<std::string> names;
for (const auto& gamepad : gamepads_) {
names.push_back(gamepad->name);
}
return names;
}
// Obten el número de mandos conectados
auto Input::getNumGamepads() const -> int { return gamepads_.size(); }
// Obtiene el gamepad a partir de un event.id
auto Input::getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Input::Gamepad> {
for (const auto& gamepad : gamepads_) {
if (gamepad->instance_id == id) {
return gamepad;
}
}
return nullptr;
}
auto Input::getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad> {
for (const auto& gamepad : gamepads_) {
if (gamepad && gamepad->name == name) {
return gamepad;
}
}
return nullptr;
}
// Obtiene el SDL_GamepadButton asignado a un action
auto Input::getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton {
return static_cast<SDL_GamepadButton>(gamepad->bindings[action].button);
}
// Convierte un InputAction a std::string
auto Input::inputToString(Action action) -> std::string {
switch (action) {
case Action::FIRE_LEFT:
return "input_fire_left";
case Action::FIRE_CENTER:
return "input_fire_center";
case Action::FIRE_RIGHT:
return "input_fire_right";
case Action::START:
return "input_start";
case Action::SERVICE:
return "input_service";
default:
return "";
}
}
// Convierte un std::string a InputAction
auto Input::stringToInput(const std::string& name) -> Action {
static const std::unordered_map<std::string, Action> INPUT_MAP = {
{"input_fire_left", Action::FIRE_LEFT},
{"input_fire_center", Action::FIRE_CENTER},
{"input_fire_right", Action::FIRE_RIGHT},
{"input_start", Action::START},
{"input_service", Action::SERVICE}};
auto it = INPUT_MAP.find(name);
return it != INPUT_MAP.end() ? it->second : Action::NONE;
}
// Comprueba el eje del mando
auto Input::checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool {
// Umbral para considerar el eje como activo
bool axis_active_now = false;
switch (action) {
case Action::LEFT:
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) < -AXIS_THRESHOLD;
break;
case Action::RIGHT:
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) > AXIS_THRESHOLD;
break;
case Action::UP:
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTY) < -AXIS_THRESHOLD;
break;
case Action::DOWN:
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTY) > AXIS_THRESHOLD;
break;
default:
return false;
}
// Referencia al binding correspondiente
auto& binding = gamepad->bindings[action];
if (repeat) {
// Si se permite repetir, simplemente devolvemos el estado actual
return axis_active_now;
} // Si no se permite repetir, aplicamos la lógica de transición
if (axis_active_now && !binding.axis_active) {
// Transición de inactivo a activo
binding.axis_active = true;
return true;
}
if (!axis_active_now && binding.axis_active) {
// Transición de activo a inactivo
binding.axis_active = false;
}
// Mantener el estado actual
return false;
}
// Comprueba los triggers del mando como botones digitales
auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool {
// Solo manejamos botones específicos que pueden ser triggers
if (gamepad->bindings[action].button != static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)) {
// Solo procesamos L2 y R2 como triggers
int button = gamepad->bindings[action].button;
// Verificar si el botón mapeado corresponde a un trigger virtual
// (Para esto necesitamos valores especiales que representen L2/R2 como botones)
bool trigger_active_now = false;
// Usamos constantes especiales para L2 y R2 como botones
if (button == TRIGGER_L2_AS_BUTTON) { // L2 como botón
Sint16 trigger_value = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER);
trigger_active_now = trigger_value > TRIGGER_THRESHOLD;
} else if (button == TRIGGER_R2_AS_BUTTON) { // R2 como botón
Sint16 trigger_value = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER);
trigger_active_now = trigger_value > TRIGGER_THRESHOLD;
} else {
return false; // No es un trigger
}
// Referencia al binding correspondiente
auto& binding = gamepad->bindings[action];
if (repeat) {
// Si se permite repetir, simplemente devolvemos el estado actual
return trigger_active_now;
}
// Si no se permite repetir, aplicamos la lógica de transición
if (trigger_active_now && !binding.trigger_active) {
// Transición de inactivo a activo
binding.trigger_active = true;
return true;
}
if (!trigger_active_now && binding.trigger_active) {
// Transición de activo a inactivo
binding.trigger_active = false;
}
// Mantener el estado actual
return false;
}
return false;
}
void Input::addGamepadMappingsFromFile() {
if (SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str()) < 0) {
std::cout << "Error, could not load " << gamepad_mappings_file_.c_str() << " file: " << SDL_GetError() << '\n';
}
}
void Input::discoverGamepads() {
// Enumera los gamepads ya conectados sin drenar la cola de eventos de SDL
// (necesario con SDL_MAIN_USE_CALLBACKS, que entrega los eventos por SDL_AppEvent).
int count = 0;
SDL_JoystickID* joysticks = SDL_GetGamepads(&count);
if (joysticks == nullptr) {
return;
}
for (int i = 0; i < count; ++i) {
addGamepad(joysticks[i]);
}
SDL_free(joysticks);
}
void Input::initSDLGamePad() {
if (SDL_WasInit(SDL_INIT_GAMEPAD) != 1) {
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
std::cout << "SDL_GAMEPAD could not initialize! SDL Error: " << SDL_GetError() << '\n';
} else {
addGamepadMappingsFromFile();
loadGamepadConfigs();
discoverGamepads();
}
}
}
void Input::resetInputStates() {
// Resetear todos los KeyBindings.active a false
for (auto& key : keyboard_.bindings) {
key.second.is_held = false;
key.second.just_pressed = false;
}
// Resetear todos los ControllerBindings.active a false
for (auto& gamepad : gamepads_) {
for (auto& binding : gamepad->bindings) {
binding.second.is_held = false;
binding.second.just_pressed = false;
binding.second.trigger_active = false;
}
}
}
void Input::update() {
// --- TECLADO ---
const bool* key_states = SDL_GetKeyboardState(nullptr);
for (auto& binding : keyboard_.bindings) {
bool key_is_down_now = key_states[binding.second.scancode];
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
binding.second.just_pressed = key_is_down_now && !binding.second.is_held;
binding.second.is_held = key_is_down_now;
}
// --- MANDOS ---
for (const auto& gamepad : gamepads_) {
for (auto& binding : gamepad->bindings) {
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
binding.second.is_held = button_is_down_now;
}
}
}
auto Input::handleEvent(const SDL_Event& event) -> std::string {
switch (event.type) {
case SDL_EVENT_GAMEPAD_ADDED:
return addGamepad(event.gdevice.which);
case SDL_EVENT_GAMEPAD_REMOVED:
return removeGamepad(event.gdevice.which);
}
return {};
}
auto Input::addGamepad(int device_index) -> std::string {
SDL_Gamepad* pad = SDL_OpenGamepad(device_index);
if (pad == nullptr) {
std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n';
return {};
}
auto gamepad = std::make_shared<Gamepad>(pad);
auto name = gamepad->name;
std::cout << "Gamepad connected (" << name << ")" << '\n';
applyGamepadConfig(gamepad);
saveGamepadConfigFromGamepad(gamepad);
gamepads_.push_back(std::move(gamepad));
return name + " CONNECTED";
}
auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
auto it = std::ranges::find_if(gamepads_, [id](const std::shared_ptr<Gamepad>& gamepad) -> bool {
return gamepad->instance_id == id;
});
if (it != gamepads_.end()) {
std::string name = (*it)->name;
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
gamepads_.erase(it);
return name + " DISCONNECTED";
}
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
return {};
}
void Input::printConnectedGamepads() const {
if (gamepads_.empty()) {
std::cout << "No hay gamepads conectados." << '\n';
return;
}
std::cout << "Gamepads conectados:\n";
for (const auto& gamepad : gamepads_) {
std::string name = gamepad->name.empty() ? "Desconocido" : gamepad->name;
std::cout << " - ID: " << gamepad->instance_id
<< ", Nombre: " << name << ")" << '\n';
}
}
void Input::loadGamepadConfigs() {
if (GamepadConfigManager::fileExists(gamepad_configs_file_)) {
GamepadConfigManager::readFromJson(gamepad_configs_, gamepad_configs_file_);
}
}
void Input::saveGamepadConfigs() {
GamepadConfigManager::writeToJson(gamepad_configs_, gamepad_configs_file_);
}
void Input::applyGamepadConfig(std::shared_ptr<Gamepad> gamepad) {
if (!gamepad || gamepad->path.empty()) { // No podemos aplicar config sin una ruta
return;
}
// --- Buscar configuración por RUTA (path) ---
auto config_it = std::ranges::find_if(gamepad_configs_, [&gamepad](const GamepadConfig& config) -> bool {
return config.path == gamepad->path;
});
if (config_it != gamepad_configs_.end()) {
// Se encontró una configuración específica para este puerto/dispositivo. La aplicamos.
std::cout << "Applying custom config for gamepad at path: " << gamepad->path << '\n';
for (const auto& [action, button] : config_it->bindings) {
if (gamepad->bindings.contains(action)) {
gamepad->bindings[action].button = button;
}
}
}
// Opcional: Podrías añadir un fallback para buscar por nombre si no se encuentra por ruta.
}
void Input::saveGamepadConfigFromGamepad(std::shared_ptr<Gamepad> gamepad) {
if (!gamepad || gamepad->path.empty()) { // No podemos guardar una config sin una ruta
return;
}
// --- CAMBIO CLAVE: Buscar si ya existe una configuración por RUTA (path) ---
auto config_it = std::ranges::find_if(gamepad_configs_, [&gamepad](const GamepadConfig& config) -> bool {
return config.path == gamepad->path;
});
// Crear nueva configuración desde el gamepad, incluyendo nombre y ruta
GamepadConfig new_config(gamepad->name, gamepad->path); // <--- CAMBIO: Pasamos ambos
new_config.bindings.clear();
// Copiar todos los bindings actuales del gamepad
for (const auto& [action, buttonState] : gamepad->bindings) {
new_config.bindings[action] = static_cast<SDL_GamepadButton>(buttonState.button);
}
if (config_it != gamepad_configs_.end()) {
// Sobreescribir configuración existente para esta ruta
*config_it = new_config;
} else {
// Añadir nueva configuración
gamepad_configs_.push_back(new_config);
}
// Guardar cambios inmediatamente
saveGamepadConfigs();
}
// Método para establecer el archivo de configuración (opcional)
void Input::setGamepadConfigsFile(const std::string& filename) {
gamepad_configs_file_ = filename;
loadGamepadConfigs(); // Recargar con el nuevo archivo
}
// Método para obtener configuración de un gamepad específico (opcional)
auto Input::getGamepadConfig(const std::string& gamepad_name) -> GamepadConfig* {
auto config_it = std::ranges::find_if(gamepad_configs_, [&gamepad_name](const GamepadConfig& config) -> bool {
return config.name == gamepad_name;
});
return (config_it != gamepad_configs_.end()) ? &(*config_it) : nullptr;
}
// Método para eliminar configuración de gamepad (opcional)
auto Input::removeGamepadConfig(const std::string& gamepad_name) -> bool {
auto config_it = std::ranges::find_if(gamepad_configs_, [&gamepad_name](const GamepadConfig& config) -> bool {
return config.name == gamepad_name;
});
if (config_it != gamepad_configs_.end()) {
gamepad_configs_.erase(config_it);
saveGamepadConfigs();
return true;
}
return false;
}
auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Input::Gamepad> {
// Si no hay gamepads disponibles, devolver gamepad por defecto
if (gamepads_.empty()) {
return nullptr;
}
// Buscar por nombre
for (const auto& gamepad : gamepads_) {
if (gamepad && gamepad->name == gamepad_name) {
return gamepad;
}
}
// Si no se encuentra por nombre, devolver el primer gamepad válido
for (const auto& gamepad : gamepads_) {
if (gamepad) {
return gamepad;
}
}
// Si llegamos aquí, no hay gamepads válidos
return nullptr;
}

227
source/core/input/input.hpp Normal file
View File

@@ -0,0 +1,227 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_Scancode, SDL_GamepadButton, SDL_JoystickID, SDL_CloseGamepad, SDL_Gamepad, SDL_GetGamepadJoystick, SDL_GetGamepadName, SDL_GetGamepadPath, SDL_GetJoystickID, Sint16, Uint8, SDL_Event
#include <array> // Para array
#include <memory> // Para shared_ptr
#include <string> // Para string, basic_string
#include <unordered_map> // Para unordered_map
#include <utility> // Para pair
#include <vector> // Para vector
#include "gamepad_config_manager.hpp" // for GamepadConfig (ptr only), GamepadConfigs
#include "input_types.hpp" // for InputAction
// --- Clase Input: gestiona la entrada de teclado y mandos (singleton) ---
class Input {
public:
// --- Constantes ---
static constexpr bool ALLOW_REPEAT = true; // Permite repetición
static constexpr bool DO_NOT_ALLOW_REPEAT = false; // No permite repetición
static constexpr bool CHECK_KEYBOARD = true; // Comprueba teclado
static constexpr bool DO_NOT_CHECK_KEYBOARD = false; // No comprueba teclado
static constexpr int TRIGGER_L2_AS_BUTTON = 100; // L2 como botón
static constexpr int TRIGGER_R2_AS_BUTTON = 101; // R2 como botón
// --- Tipos ---
using Action = InputAction; // Alias para mantener compatibilidad
// --- Estructuras ---
struct KeyState {
Uint8 scancode; // Scancode asociado
bool is_held; // Está pulsada ahora mismo
bool just_pressed; // Se acaba de pulsar en este fotograma
KeyState(Uint8 scancode = 0, bool is_held = false, bool just_pressed = false)
: scancode(scancode),
is_held(is_held),
just_pressed(just_pressed) {}
};
struct ButtonState {
int button; // GameControllerButton asociado
bool is_held; // Está pulsada ahora mismo
bool just_pressed; // Se acaba de pulsar en este fotograma
bool axis_active; // Estado del eje
bool trigger_active{false}; // Estado del trigger como botón digital
ButtonState(int btn = static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID), bool is_held = false, bool just_pressed = false, bool axis_act = false)
: button(btn),
is_held(is_held),
just_pressed(just_pressed),
axis_active(axis_act) {}
};
struct Keyboard {
std::unordered_map<Action, KeyState> bindings{
// Movimiento del jugador
{Action::UP, KeyState(SDL_SCANCODE_UP)},
{Action::DOWN, KeyState(SDL_SCANCODE_DOWN)},
{Action::LEFT, KeyState(SDL_SCANCODE_LEFT)},
{Action::RIGHT, KeyState(SDL_SCANCODE_RIGHT)},
// Disparo del jugador
{Action::FIRE_LEFT, KeyState(SDL_SCANCODE_Q)},
{Action::FIRE_CENTER, KeyState(SDL_SCANCODE_W)},
{Action::FIRE_RIGHT, KeyState(SDL_SCANCODE_E)},
// Interfaz
{Action::START, KeyState(SDL_SCANCODE_RETURN)},
// Menu de servicio
{Action::SERVICE, KeyState(SDL_SCANCODE_F12)},
{Action::SM_SELECT, KeyState(SDL_SCANCODE_RETURN)},
{Action::SM_BACK, KeyState(SDL_SCANCODE_BACKSPACE)},
// Control del programa
{Action::EXIT, KeyState(SDL_SCANCODE_ESCAPE)},
{Action::PAUSE, KeyState(SDL_SCANCODE_P)},
{Action::BACK, KeyState(SDL_SCANCODE_BACKSPACE)},
{Action::WINDOW_DEC_SIZE, KeyState(SDL_SCANCODE_F1)},
{Action::WINDOW_INC_SIZE, KeyState(SDL_SCANCODE_F2)},
{Action::WINDOW_FULLSCREEN, KeyState(SDL_SCANCODE_F3)},
{Action::TOGGLE_VIDEO_POSTFX, KeyState(SDL_SCANCODE_F4)},
{Action::NEXT_SHADER, KeyState(SDL_SCANCODE_8)},
{Action::NEXT_POSTFX_PRESET, KeyState(SDL_SCANCODE_9)},
{Action::TOGGLE_SUPERSAMPLING, KeyState(SDL_SCANCODE_0)},
{Action::TOGGLE_VIDEO_INTEGER_SCALE, KeyState(SDL_SCANCODE_F5)},
{Action::TOGGLE_VIDEO_VSYNC, KeyState(SDL_SCANCODE_F6)},
{Action::TOGGLE_AUDIO, KeyState(SDL_SCANCODE_F7)},
{Action::TOGGLE_AUTO_FIRE, KeyState(SDL_SCANCODE_F8)},
{Action::CHANGE_LANG, KeyState(SDL_SCANCODE_F9)},
{Action::RESET, KeyState(SDL_SCANCODE_F10)},
{Action::SHOW_INFO, KeyState(SDL_SCANCODE_F11)}};
Keyboard() = default;
};
struct Gamepad {
SDL_Gamepad* pad;
SDL_JoystickID instance_id;
std::string name;
std::string path;
std::unordered_map<Action, ButtonState> bindings;
Gamepad(SDL_Gamepad* gamepad)
: pad(gamepad),
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
name(std::string(SDL_GetGamepadName(gamepad))),
path(std::string(SDL_GetGamepadPath(pad))),
bindings{
// Movimiento del jugador
{Action::UP, ButtonState(static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_UP))},
{Action::DOWN, ButtonState(static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_DOWN))},
{Action::LEFT, ButtonState(static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT))},
{Action::RIGHT, ButtonState(static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT))},
// Disparo del jugador
{Action::FIRE_LEFT, ButtonState(static_cast<int>(SDL_GAMEPAD_BUTTON_WEST))},
{Action::FIRE_CENTER, ButtonState(static_cast<int>(SDL_GAMEPAD_BUTTON_NORTH))},
{Action::FIRE_RIGHT, ButtonState(static_cast<int>(SDL_GAMEPAD_BUTTON_EAST))},
// Interfaz
{Action::START, ButtonState(static_cast<int>(SDL_GAMEPAD_BUTTON_START))},
{Action::SERVICE, ButtonState(static_cast<int>(SDL_GAMEPAD_BUTTON_BACK))},
// Menu de servicio
{Action::SM_SELECT, ButtonState(static_cast<int>(SDL_GAMEPAD_BUTTON_WEST))},
{Action::SM_BACK, ButtonState(static_cast<int>(SDL_GAMEPAD_BUTTON_NORTH))}} {}
~Gamepad() {
if (pad != nullptr) {
SDL_CloseGamepad(pad);
}
}
// Reasigna un botón a una acción
void rebindAction(Action action, SDL_GamepadButton new_button) {
bindings[action] = static_cast<int>(new_button);
}
};
// --- Tipos ---
using Gamepads = std::vector<std::shared_ptr<Gamepad>>; // Vector de gamepads
// --- Métodos de singleton ---
static void init(const std::string& game_controller_db_path, const std::string& gamepad_configs_file);
static void destroy();
static auto get() -> Input*;
// --- Métodos de configuración de controles ---
void bindKey(Action action, SDL_Scancode code);
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button);
static void bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source);
// --- Métodos de consulta de entrada ---
void update();
auto checkAction(Action action, bool repeat = true, bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
auto checkAnyInput(bool check_keyboard = true, const std::shared_ptr<Gamepad>& gamepad = nullptr) -> bool;
auto checkAnyButton(bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
// --- Métodos de gestión de mandos ---
[[nodiscard]] auto gameControllerFound() const -> bool;
static auto getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string;
[[nodiscard]] auto getControllerNames() const -> std::vector<std::string>;
[[nodiscard]] auto getNumGamepads() const -> int;
[[nodiscard]] auto getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Gamepad>;
[[nodiscard]] auto getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad>;
[[nodiscard]] auto getGamepads() const -> const Gamepads& { return gamepads_; }
// --- Métodos de consulta y utilidades ---
[[nodiscard]] static auto getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton;
[[nodiscard]] static auto inputToString(Action action) -> std::string;
[[nodiscard]] static auto stringToInput(const std::string& name) -> Action;
// --- Métodos de reseteo de estado de entrada ---
void resetInputStates();
// --- Eventos ---
auto handleEvent(const SDL_Event& event) -> std::string;
void printConnectedGamepads() const;
auto findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Gamepad>;
void saveGamepadConfigFromGamepad(std::shared_ptr<Gamepad> gamepad);
private:
// --- Constantes ---
static constexpr Sint16 AXIS_THRESHOLD = 30000;
static constexpr Sint16 TRIGGER_THRESHOLD = 16384; // Umbral para triggers (aproximadamente 50% del rango)
static constexpr std::array<Action, 4> BUTTON_INPUTS = {Action::FIRE_LEFT, Action::FIRE_CENTER, Action::FIRE_RIGHT, Action::START}; // Listado de los inputs para jugar que utilizan botones, ni palancas ni crucetas
// --- Variables internas ---
Gamepads gamepads_;
Keyboard keyboard_;
std::string gamepad_mappings_file_;
std::string gamepad_configs_file_;
GamepadConfigs gamepad_configs_;
// --- Métodos internos ---
void initSDLGamePad();
static auto checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
static auto checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool;
auto addGamepad(int device_index) -> std::string;
auto removeGamepad(SDL_JoystickID id) -> std::string;
void addGamepadMappingsFromFile();
void discoverGamepads();
// --- Métodos para integración con GamepadConfigManager ---
void loadGamepadConfigs();
void saveGamepadConfigs();
void applyGamepadConfig(std::shared_ptr<Gamepad> gamepad);
// Métodos auxiliares opcionales
void setGamepadConfigsFile(const std::string& filename);
auto getGamepadConfig(const std::string& gamepad_name) -> GamepadConfig*;
auto removeGamepadConfig(const std::string& gamepad_name) -> bool;
// --- Constructor y destructor ---
explicit Input(std::string game_controller_db_path, std::string gamepad_configs_file);
~Input() = default;
// --- Singleton ---
static Input* instance;
};

View File

@@ -0,0 +1,107 @@
#include "input_types.hpp"
#include <utility> // Para pair
// Definición de los mapas
const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
{InputAction::FIRE_LEFT, "FIRE_LEFT"},
{InputAction::FIRE_CENTER, "FIRE_CENTER"},
{InputAction::FIRE_RIGHT, "FIRE_RIGHT"},
{InputAction::START, "START"},
{InputAction::SERVICE, "SERVICE"},
{InputAction::UP, "UP"},
{InputAction::DOWN, "DOWN"},
{InputAction::LEFT, "LEFT"},
{InputAction::RIGHT, "RIGHT"},
{InputAction::SM_SELECT, "SM_SELECT"},
{InputAction::SM_BACK, "SM_BACK"},
{InputAction::BACK, "BACK"},
{InputAction::EXIT, "EXIT"},
{InputAction::PAUSE, "PAUSE"},
{InputAction::WINDOW_FULLSCREEN, "WINDOW_FULLSCREEN"},
{InputAction::WINDOW_INC_SIZE, "WINDOW_INC_SIZE"},
{InputAction::WINDOW_DEC_SIZE, "WINDOW_DEC_SIZE"},
{InputAction::TOGGLE_VIDEO_POSTFX, "TOGGLE_VIDEO_POSTFX"},
{InputAction::NEXT_SHADER, "NEXT_SHADER"},
{InputAction::NEXT_POSTFX_PRESET, "NEXT_POSTFX_PRESET"},
{InputAction::TOGGLE_SUPERSAMPLING, "TOGGLE_SUPERSAMPLING"},
{InputAction::TOGGLE_VIDEO_INTEGER_SCALE, "TOGGLE_VIDEO_INTEGER_SCALE"},
{InputAction::TOGGLE_VIDEO_VSYNC, "TOGGLE_VIDEO_VSYNC"},
{InputAction::RESET, "RESET"},
{InputAction::TOGGLE_AUDIO, "TOGGLE_AUDIO"},
{InputAction::CHANGE_LANG, "CHANGE_LANG"},
{InputAction::SHOW_INFO, "SHOW_INFO"},
{InputAction::CONFIG, "CONFIG"},
{InputAction::SWAP_CONTROLLERS, "SWAP_CONTROLLERS"},
{InputAction::TOGGLE_AUTO_FIRE, "TOGGLE_AUTO_FIRE"},
{InputAction::NONE, "NONE"}};
const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
{"FIRE_LEFT", InputAction::FIRE_LEFT},
{"FIRE_CENTER", InputAction::FIRE_CENTER},
{"FIRE_RIGHT", InputAction::FIRE_RIGHT},
{"START", InputAction::START},
{"SERVICE", InputAction::SERVICE},
{"UP", InputAction::UP},
{"DOWN", InputAction::DOWN},
{"LEFT", InputAction::LEFT},
{"RIGHT", InputAction::RIGHT},
{"SM_SELECT", InputAction::SM_SELECT},
{"SM_BACK", InputAction::SM_BACK},
{"BACK", InputAction::BACK},
{"EXIT", InputAction::EXIT},
{"PAUSE", InputAction::PAUSE},
{"WINDOW_FULLSCREEN", InputAction::WINDOW_FULLSCREEN},
{"WINDOW_INC_SIZE", InputAction::WINDOW_INC_SIZE},
{"WINDOW_DEC_SIZE", InputAction::WINDOW_DEC_SIZE},
{"TOGGLE_VIDEO_POSTFX", InputAction::TOGGLE_VIDEO_POSTFX},
{"NEXT_SHADER", InputAction::NEXT_SHADER},
{"NEXT_POSTFX_PRESET", InputAction::NEXT_POSTFX_PRESET},
{"TOGGLE_SUPERSAMPLING", InputAction::TOGGLE_SUPERSAMPLING},
{"TOGGLE_VIDEO_INTEGER_SCALE", InputAction::TOGGLE_VIDEO_INTEGER_SCALE},
{"TOGGLE_VIDEO_VSYNC", InputAction::TOGGLE_VIDEO_VSYNC},
{"RESET", InputAction::RESET},
{"TOGGLE_AUDIO", InputAction::TOGGLE_AUDIO},
{"CHANGE_LANG", InputAction::CHANGE_LANG},
{"SHOW_INFO", InputAction::SHOW_INFO},
{"CONFIG", InputAction::CONFIG},
{"SWAP_CONTROLLERS", InputAction::SWAP_CONTROLLERS},
{"TOGGLE_AUTO_FIRE", InputAction::TOGGLE_AUTO_FIRE},
{"NONE", InputAction::NONE}};
const std::unordered_map<SDL_GamepadButton, std::string> BUTTON_TO_STRING = {
{SDL_GAMEPAD_BUTTON_WEST, "WEST"},
{SDL_GAMEPAD_BUTTON_NORTH, "NORTH"},
{SDL_GAMEPAD_BUTTON_EAST, "EAST"},
{SDL_GAMEPAD_BUTTON_SOUTH, "SOUTH"},
{SDL_GAMEPAD_BUTTON_START, "START"},
{SDL_GAMEPAD_BUTTON_BACK, "BACK"},
{SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, "LEFT_SHOULDER"},
{SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, "RIGHT_SHOULDER"},
{SDL_GAMEPAD_BUTTON_DPAD_UP, "DPAD_UP"},
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, "DPAD_DOWN"},
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, "DPAD_LEFT"},
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, "DPAD_RIGHT"},
{static_cast<SDL_GamepadButton>(100), "L2_AS_BUTTON"},
{static_cast<SDL_GamepadButton>(101), "R2_AS_BUTTON"}};
const std::unordered_map<std::string, SDL_GamepadButton> STRING_TO_BUTTON = {
{"WEST", SDL_GAMEPAD_BUTTON_WEST},
{"NORTH", SDL_GAMEPAD_BUTTON_NORTH},
{"EAST", SDL_GAMEPAD_BUTTON_EAST},
{"SOUTH", SDL_GAMEPAD_BUTTON_SOUTH},
{"START", SDL_GAMEPAD_BUTTON_START},
{"BACK", SDL_GAMEPAD_BUTTON_BACK},
{"LEFT_SHOULDER", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{"RIGHT_SHOULDER", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{"DPAD_UP", SDL_GAMEPAD_BUTTON_DPAD_UP},
{"DPAD_DOWN", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{"DPAD_LEFT", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
{"DPAD_RIGHT", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{"L2_AS_BUTTON", static_cast<SDL_GamepadButton>(100)},
{"R2_AS_BUTTON", static_cast<SDL_GamepadButton>(101)}};
const std::unordered_map<InputAction, InputAction> ACTION_TO_ACTION = {
{InputAction::SM_SELECT, InputAction::FIRE_LEFT},
{InputAction::SM_BACK, InputAction::FIRE_CENTER},
};

View File

@@ -0,0 +1,58 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include <unordered_map>
// --- Enums ---
enum class InputAction : int { // Acciones de entrada posibles en el juego
// Inputs de movimiento
UP,
DOWN,
LEFT,
RIGHT,
// Inputs personalizados
FIRE_LEFT,
FIRE_CENTER,
FIRE_RIGHT,
START,
// Service Menu
SM_SELECT,
SM_BACK,
// Inputs de control
BACK,
EXIT,
PAUSE,
SERVICE,
WINDOW_FULLSCREEN,
WINDOW_INC_SIZE,
WINDOW_DEC_SIZE,
TOGGLE_VIDEO_POSTFX,
NEXT_SHADER,
NEXT_POSTFX_PRESET,
TOGGLE_SUPERSAMPLING,
TOGGLE_VIDEO_INTEGER_SCALE,
TOGGLE_VIDEO_VSYNC,
RESET,
TOGGLE_AUDIO,
CHANGE_LANG,
SHOW_INFO,
CONFIG,
SWAP_CONTROLLERS,
TOGGLE_AUTO_FIRE,
// Input obligatorio
NONE,
SIZE,
};
// --- Variables ---
extern const std::unordered_map<InputAction, std::string> ACTION_TO_STRING; // Mapeo de acción a string
extern const std::unordered_map<std::string, InputAction> STRING_TO_ACTION; // Mapeo de string a acción
extern const std::unordered_map<SDL_GamepadButton, std::string> BUTTON_TO_STRING; // Mapeo de botón a string
extern const std::unordered_map<std::string, SDL_GamepadButton> STRING_TO_BUTTON; // Mapeo de string a botón
extern const std::unordered_map<InputAction, InputAction> ACTION_TO_ACTION; // Mapeo de acción a acción

View File

@@ -0,0 +1,27 @@
#include "mouse.hpp"
#include <SDL3/SDL.h> // Para SDL_GetTicks, Uint32, SDL_HideCursor, SDL_Show...
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
void handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_MOUSE_MOTION) {
last_mouse_move_time = SDL_GetTicks();
if (!cursor_visible) {
SDL_ShowCursor();
cursor_visible = true;
}
}
}
void updateCursorVisibility() {
Uint32 current_time = SDL_GetTicks();
if (cursor_visible && (current_time - last_mouse_move_time > cursor_hide_time)) {
SDL_HideCursor();
cursor_visible = false;
}
}
} // namespace Mouse

View File

@@ -0,0 +1,15 @@
#pragma once
#include <SDL3/SDL.h> // Para Uint32, SDL_Event
// --- Namespace Mouse: gestión del ratón ---
namespace Mouse {
// --- Variables de estado del cursor ---
extern Uint32 cursor_hide_time; // Tiempo en milisegundos para ocultar el cursor tras inactividad
extern Uint32 last_mouse_move_time; // Última vez (en ms) que el ratón se movió
extern bool cursor_visible; // Indica si el cursor está visible
// --- Funciones ---
void handleEvent(const SDL_Event& event); // Procesa eventos de ratón (movimiento, clic, etc.)
void updateCursorVisibility(); // Actualiza la visibilidad del cursor según la inactividad
} // namespace Mouse

View File

@@ -0,0 +1,135 @@
#pragma once
#include <cstdint>
#include <functional>
#include <string>
#include <utility>
// --- Clase PauseManager: maneja el sistema de pausa del juego ---
class PauseManager {
public:
// --- Enums ---
enum class Source : uint8_t { // Fuentes de pausa
NONE = 0,
PLAYER = 1 << 0, // 0001
SERVICE_MENU = 1 << 1, // 0010
FOCUS_LOST = 1 << 2 // 0100
};
// --- Operadores friend ---
friend auto operator|(Source a, Source b) -> Source {
return static_cast<Source>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b)); // NOLINT(readability-redundant-casting)
}
friend auto operator&(Source a, Source b) -> Source {
return static_cast<Source>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b)); // NOLINT(readability-redundant-casting)
}
friend auto operator~(Source a) -> uint8_t {
return ~static_cast<uint8_t>(a);
}
friend auto operator&=(Source& a, uint8_t b) -> Source& {
a = static_cast<Source>(static_cast<uint8_t>(a) & b);
return a;
}
friend auto operator|=(Source& a, Source b) -> Source& {
return a = a | b;
}
friend auto operator&=(Source& a, Source b) -> Source& {
return a = a & b;
}
// --- Constructor y destructor ---
explicit PauseManager(std::function<void(bool)> callback = nullptr) // Constructor con callback opcional
: on_pause_changed_callback_(std::move(callback)) {}
// --- Métodos principales ---
void setFlag(Source source, bool enable) { // Establece/quita una fuente de pausa específica
bool was_paused = isPaused();
if (enable) {
flags_ |= source;
} else {
flags_ &= ~source; // Ahora funciona: Source &= uint8_t
}
if (was_paused != isPaused()) {
notifyPauseChanged();
}
}
// --- Métodos específicos para cada fuente ---
void setPlayerPause(bool enable) { setFlag(Source::PLAYER, enable); }
void setServiceMenuPause(bool enable) { setFlag(Source::SERVICE_MENU, enable); }
void setFocusLossPause(bool enable) { setFlag(Source::FOCUS_LOST, enable); }
void togglePlayerPause() { setPlayerPause(!isPlayerPaused()); } // Toggle para el jugador (más común)
// --- Getters ---
[[nodiscard]] auto isPaused() const -> bool { return flags_ != Source::NONE; }
[[nodiscard]] auto isPlayerPaused() const -> bool { return hasFlag(Source::PLAYER); }
[[nodiscard]] auto isServiceMenuPaused() const -> bool { return hasFlag(Source::SERVICE_MENU); }
[[nodiscard]] auto isFocusLossPaused() const -> bool { return hasFlag(Source::FOCUS_LOST); }
[[nodiscard]] auto getFlags() const -> Source { return flags_; } // Obtiene las banderas actuales
// --- Métodos de utilidad ---
void clearAll() { // Limpia todas las pausas (útil para reset)
if (isPaused()) {
flags_ = Source::NONE;
notifyPauseChanged();
}
}
[[nodiscard]] auto getStatusString() const -> std::string { // Método para debug
if (flags_ == Source::NONE) {
return "Active";
}
std::string result = "Paused by: ";
bool first = true;
if (hasFlag(Source::PLAYER)) {
if (!first) {
result += ", ";
}
result += "Player";
first = false;
}
if (hasFlag(Source::SERVICE_MENU)) {
if (!first) {
result += ", ";
}
result += "ServiceMenu";
first = false;
}
if (hasFlag(Source::FOCUS_LOST)) {
if (!first) {
result += ", ";
}
result += "FocusLoss";
}
return result;
}
void setCallback(std::function<void(bool)> callback) { // Permite cambiar el callback en runtime
on_pause_changed_callback_ = std::move(callback);
}
private:
// --- Variables ---
Source flags_ = Source::NONE;
std::function<void(bool)> on_pause_changed_callback_;
// --- Métodos internos ---
[[nodiscard]] auto hasFlag(Source flag) const -> bool {
return (flags_ & flag) != Source::NONE;
}
void notifyPauseChanged() {
if (on_pause_changed_callback_) {
on_pause_changed_callback_(isPaused());
}
}
};

204
source/core/locale/lang.cpp Normal file
View File

@@ -0,0 +1,204 @@
#include "lang.hpp"
#include <cstddef> // Para size_t
#include <exception> // Para exception
#include <fstream> // Para basic_ifstream, basic_istream, ifstream
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==
#include <utility> // Para pair
#include <vector> // Para vector
#include "asset.hpp" // Para Asset
#include "difficulty.hpp" // Para Difficulty
#include "external/json.hpp" // Para basic_json, iteration_proxy_value, oper...
#include "options.hpp" // Para SettingsOpt...
#include "resource_helper.hpp" // Para ResourceHelper
using json = nlohmann::json;
namespace Lang {
std::unordered_map<std::string, std::string> texts;
// Vector con los idiomas soportados
std::vector<Language> languages = {
{Code::SPANISH, "Castellano", "es_ES.json"},
{Code::VALENCIAN, "Balooncia", "ba_BA.json"},
{Code::ENGLISH, "Ingles", "en_UK.json"}};
// Inicializa los textos del juego en el idioma seleccionado
auto loadFromFile(const std::string& file_path) -> bool {
texts.clear();
// Intentar cargar desde ResourceHelper primero
auto resource_data = ResourceHelper::loadFile(file_path);
try {
json j;
if (!resource_data.empty()) {
// Cargar desde datos del pack
std::string content(resource_data.begin(), resource_data.end());
j = json::parse(content);
} else {
// Fallback a filesystem directo
std::ifstream rfile(file_path);
if (!rfile.is_open()) {
return false;
}
rfile >> j;
}
for (const auto& el : j.items()) {
texts[el.key()] = el.value();
}
} catch (const std::exception& e) {
// Puedes loguear el error si quieres
return false;
}
return true;
}
// Obtiene el texto por clave
auto getText(const std::string& key) -> std::string {
auto it = texts.find(key);
if (it != texts.end()) {
return it->second;
}
return "[missing text: " + key + "]";
}
// Obtiene el código del siguiente idioma disponible
auto getNextLangCode(Code lang) -> Code {
for (size_t i = 0; i < languages.size(); ++i) {
if (languages[i].code == lang) {
return languages[(i + 1) % languages.size()].code;
}
}
// Si no se encuentra, devuelve el primero por defecto
return languages[0].code;
}
// Obtiene un idioma del vector de idiomas a partir de un código
auto getLanguage(Code code) -> Language {
for (const auto& lang : languages) {
if (lang.code == code) {
return lang;
}
}
// Si no se encuentra, devuelve el primero por defecto
return languages[0];
}
// Devuelve el código de un idioma a partir de un nombre
auto getCodeFromName(const std::string& name) -> Code {
for (const auto& lang : languages) {
if (lang.name == name) {
return lang.code;
}
}
// Si no se encuentra, devuelve el primero por defecto
return languages[0].code;
}
// Devuelve el nombre de un idioma a partir de un código
auto getNameFromCode(Code code) -> std::string {
for (const auto& lang : languages) {
if (lang.code == code) {
return lang.name;
}
}
// Si no se encuentra, devuelve el nombre del primer idioma por defecto
return languages[0].name;
}
// Actualiza los nombres de los idiomas
void updateLanguageNames() {
for (auto& lang : languages) {
switch (lang.code) {
case Code::SPANISH:
lang.name = Lang::getText("[SERVICE_MENU] LANG_ES");
break;
case Code::VALENCIAN:
lang.name = Lang::getText("[SERVICE_MENU] LANG_BA");
break;
case Code::ENGLISH:
lang.name = Lang::getText("[SERVICE_MENU] LANG_EN");
break;
default:
lang.name = "Unknown";
break;
}
}
}
// Actualiza los nombres de las dificultades
void updateDifficultyNames() {
// 1. Pide una referencia MODIFICABLE a la lista de dificultades
auto& difficulties = Difficulty::getDifficulties();
// 2. Recorre la lista
for (auto& difficulty_info : difficulties) {
// 3. Para cada dificultad, usa su código para obtener el texto traducido y actualizar su nombre
switch (difficulty_info.code) {
case Difficulty::Code::EASY:
difficulty_info.name = Lang::getText("[SERVICE_MENU] EASY");
break;
case Difficulty::Code::NORMAL:
difficulty_info.name = Lang::getText("[SERVICE_MENU] NORMAL");
break;
case Difficulty::Code::HARD:
difficulty_info.name = Lang::getText("[SERVICE_MENU] HARD");
break;
}
}
}
// Obtiene una fichero a partir de un lang::Code
auto getLanguageFileName(Lang::Code code) -> std::string {
for (const auto& lang : languages) {
if (lang.code == code) {
return Asset::get()->getPath(lang.file_name);
}
}
// Si no se encuentra, devuelve el fichero del primer idioma por defecto
return Asset::get()->getPath(languages[0].file_name);
}
// Establece el idioma
void setLanguage(Code code) {
Options::settings.language = code;
loadFromFile(Asset::get()->getPath(getLanguage(code).file_name));
updateLanguageNames();
updateDifficultyNames();
}
// Obtiene una fichero a partir de un Code
auto getLangFile(Code code) -> std::string {
switch (code) {
case Code::VALENCIAN:
return Asset::get()->getPath("ba_BA.json");
break;
case Code::SPANISH:
return Asset::get()->getPath("es_ES.json");
break;
default:
return Asset::get()->getPath("en_UK.json");
break;
}
}
// Obtiene una cadena a partir de un Code
auto getLangName(Code code) -> std::string {
switch (code) {
case Code::VALENCIAN:
return " \"ba_BA\"";
break;
case Code::SPANISH:
return " \"es_ES\"";
break;
default:
return " \"en_UK\"";
break;
}
}
} // namespace Lang

View File

@@ -0,0 +1,39 @@
#pragma once
#include <string> // Para string, basic_string
#include <utility> // Para move
// --- Namespace Lang: gestión de idiomas y textos ---
namespace Lang {
// --- Enums ---
enum class Code : int {
SPANISH = 0, // Español
VALENCIAN = 1, // Valenciano
ENGLISH = 2 // Inglés
};
// --- Estructuras ---
struct Language {
Code code; // Código que identifica al idioma
std::string name; // Nombre que identifica el idioma
std::string file_name; // Nombre del fichero con los textos
Language(Code c, std::string n, std::string fn)
: code(c),
name(std::move(n)),
file_name(std::move(fn)) {}
};
// --- Funciones ---
auto loadFromFile(const std::string& file_path) -> bool; // Carga los textos desde el fichero JSON especificado
auto getText(const std::string& key) -> std::string; // Obtiene el texto por clave
auto getNextLangCode(Code current_lang) -> Code; // Obtiene el código del siguiente idioma (circular)
auto getLanguage(Code code) -> Language; // Obtiene el idioma correspondiente al código proporcionado
auto getCodeFromName(const std::string& name) -> Code; // Devuelve el código de un idioma a partir de un nombre
auto getNameFromCode(Code code) -> std::string; // Devuelve el nombre de un idioma a partir de un código
void updateLanguageNames(); // Actualiza los nombres de los idiomas
auto getLanguageFileName(Code code) -> std::string; // Obtiene el nombre del fichero de textos asociado a un código de idioma
void setLanguage(Code code); // Establece el idioma actual
auto getLangFile(Code code) -> std::string; // Obtiene una fichero a partir de un Code
auto getLangName(Code code) -> std::string; // Obtiene una cadena a partir de un Code
} // namespace Lang

View File

@@ -0,0 +1,575 @@
#define _USE_MATH_DEFINES
#include "background.hpp"
#include <SDL3/SDL.h> // Para SDL_FRect, SDL_SetRenderTarget, SDL_CreateTexture, SDL_DestroyTexture, SDL_GetRenderTarget, SDL_RenderTexture, SDL_SetTextureAlphaMod, SDL_SetTextureBlendMode, SDL_BLENDMODE_BLEND, SDL_PixelFormat, SDL_RenderClear, SDL_SetRenderDrawColor, SDL_TextureAccess, SDL_FPoint
#include <algorithm> // Para clamp, min, max
#include <cmath> // Para M_PI, cos, sin
#include <string> // Para basic_string
#include <utility> // Para move
#include "animated_sprite.hpp" // Para AnimatedSprite
#include "moving_sprite.hpp" // Para MovingSprite
#include "param.hpp" // Para Param, ParamBackground, param
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "sprite.hpp" // Para Sprite
#include "texture.hpp" // Para Texture
#include "utils.hpp" // Para easeOutCubic
// Constructor
Background::Background(float total_progress_to_complete)
: renderer_(Screen::get()->getRenderer()),
buildings_texture_(Resource::get()->getTexture("game_buildings.png")),
top_clouds_texture_(Resource::get()->getTexture("game_clouds1.png")),
bottom_clouds_texture_(Resource::get()->getTexture("game_clouds2.png")),
gradients_texture_(Resource::get()->getTexture("game_sky_colors.png")),
sun_texture_(Resource::get()->getTexture("game_sun.png")),
moon_texture_(Resource::get()->getTexture("game_moon.png")),
grass_sprite_(std::make_unique<AnimatedSprite>(Resource::get()->getTexture("game_grass.png"), Resource::get()->getAnimation("game_grass.ani"))),
total_progress_to_complete_(total_progress_to_complete),
progress_per_stage_(total_progress_to_complete_ / STAGES),
sun_completion_progress_(total_progress_to_complete_ * SUN_COMPLETION_FACTOR),
minimum_completed_progress_(total_progress_to_complete_ * MINIMUM_COMPLETED_PROGRESS_PERCENTAGE),
rect_(SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(gradients_texture_->getWidth() / 2), .h = static_cast<float>(gradients_texture_->getHeight() / 2)}),
src_rect_({.x = 0, .y = 0, .w = 320, .h = 240}),
dst_rect_({.x = 0, .y = 0, .w = 320, .h = 240}),
attenuate_color_(Color(param.background.attenuate_color.r, param.background.attenuate_color.g, param.background.attenuate_color.b)),
alpha_color_texture_(param.background.attenuate_color.a),
previous_alpha_color_texture_(param.background.attenuate_color.a),
base_(rect_.h) {
initializePaths();
initializeRects();
initializeSprites();
initializeSpriteProperties();
initializeTextures();
}
// Destructor
Background::~Background() {
SDL_DestroyTexture(canvas_);
SDL_DestroyTexture(color_texture_);
}
// Inicializa las rutas del sol y la luna
void Background::initializePaths() {
createSunPath();
createMoonPath();
}
// Inicializa los rectángulos de gradientes y nubes
void Background::initializeRects() {
gradient_rect_[0] = {.x = 0, .y = 0, .w = rect_.w, .h = rect_.h};
gradient_rect_[1] = {.x = rect_.w, .y = 0, .w = rect_.w, .h = rect_.h};
gradient_rect_[2] = {.x = 0, .y = rect_.h, .w = rect_.w, .h = rect_.h};
gradient_rect_[3] = {.x = rect_.w, .y = rect_.h, .w = rect_.w, .h = rect_.h};
const float TOP_CLOUDS_TEXTURE_HEIGHT = top_clouds_texture_->getHeight() / 4;
const float BOTTOM_CLOUDS_TEXTURE_HEIGHT = bottom_clouds_texture_->getHeight() / 4;
for (int i = 0; i < 4; ++i) {
top_clouds_rect_[i] = {.x = 0, .y = i * TOP_CLOUDS_TEXTURE_HEIGHT, .w = static_cast<float>(top_clouds_texture_->getWidth()), .h = TOP_CLOUDS_TEXTURE_HEIGHT};
bottom_clouds_rect_[i] = {.x = 0, .y = i * BOTTOM_CLOUDS_TEXTURE_HEIGHT, .w = static_cast<float>(bottom_clouds_texture_->getWidth()), .h = BOTTOM_CLOUDS_TEXTURE_HEIGHT};
}
}
// Crea los sprites
void Background::initializeSprites() {
const float TOP_CLOUDS_Y = base_ - 165;
const float BOTTOM_CLOUDS_Y = base_ - 101;
top_clouds_sprite_a_ = std::make_unique<MovingSprite>(top_clouds_texture_, (SDL_FRect){.x = 0, .y = TOP_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(top_clouds_texture_->getHeight())});
top_clouds_sprite_b_ = std::make_unique<MovingSprite>(top_clouds_texture_, (SDL_FRect){.x = rect_.w, .y = TOP_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(top_clouds_texture_->getHeight())});
bottom_clouds_sprite_a_ = std::make_unique<MovingSprite>(bottom_clouds_texture_, (SDL_FRect){.x = 0, .y = BOTTOM_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(bottom_clouds_texture_->getHeight())});
bottom_clouds_sprite_b_ = std::make_unique<MovingSprite>(bottom_clouds_texture_, (SDL_FRect){.x = rect_.w, .y = BOTTOM_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(bottom_clouds_texture_->getHeight())});
buildings_sprite_ = std::make_unique<Sprite>(buildings_texture_);
gradient_sprite_ = std::make_unique<Sprite>(gradients_texture_, 0, 0, rect_.w, rect_.h);
sun_sprite_ = std::make_unique<Sprite>(sun_texture_);
moon_sprite_ = std::make_unique<Sprite>(moon_texture_);
}
// Configura las propiedades iniciales de los sprites
void Background::initializeSpriteProperties() {
// Velocidades iniciales que coinciden con updateCloudsSpeed() cuando progress=0
constexpr float INITIAL_TOP_CLOUDS_SPEED_PX_PER_S = 0.05F * 60.0F; // 3.0 píxeles/segundo (coincide con CLOUDS_INITIAL_SPEED)
constexpr float INITIAL_BOTTOM_CLOUDS_SPEED_PX_PER_S = 0.05F * 60.0F / 2.0F; // 1.5 píxeles/segundo (mitad de velocidad)
top_clouds_sprite_a_->setSpriteClip(0, 0, top_clouds_texture_->getWidth(), top_clouds_texture_->getHeight());
top_clouds_sprite_a_->setVelX(-INITIAL_TOP_CLOUDS_SPEED_PX_PER_S);
top_clouds_sprite_b_->setSpriteClip(0, 0, top_clouds_texture_->getWidth(), top_clouds_texture_->getHeight());
top_clouds_sprite_b_->setVelX(-INITIAL_TOP_CLOUDS_SPEED_PX_PER_S);
bottom_clouds_sprite_a_->setSpriteClip(0, 0, bottom_clouds_texture_->getWidth(), bottom_clouds_texture_->getHeight());
bottom_clouds_sprite_a_->setVelX(-INITIAL_BOTTOM_CLOUDS_SPEED_PX_PER_S);
bottom_clouds_sprite_b_->setSpriteClip(0, 0, bottom_clouds_texture_->getWidth(), bottom_clouds_texture_->getHeight());
bottom_clouds_sprite_b_->setVelX(-INITIAL_BOTTOM_CLOUDS_SPEED_PX_PER_S);
// grass_sprite_->setY(base_ - grass_sprite_->getHeight());
// grass_sprite_->resetAnimation();
grass_sprite_->setPos(0.0F, base_ - 10.0F);
grass_sprite_->setWidth(320.0F);
grass_sprite_->setHeight(10.0F);
// grass_sprite_->setCurrentAnimation(0);
buildings_sprite_->setY(base_ - buildings_sprite_->getHeight());
sun_sprite_->setPosition(sun_path_.front());
moon_sprite_->setPosition(moon_path_.front());
}
// Inicializa las texturas de renderizado
void Background::initializeTextures() {
canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, rect_.w, rect_.h);
SDL_SetTextureBlendMode(canvas_, SDL_BLENDMODE_BLEND);
color_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, rect_.w, rect_.h);
SDL_SetTextureBlendMode(color_texture_, SDL_BLENDMODE_BLEND);
setColor(attenuate_color_);
SDL_SetTextureAlphaMod(color_texture_, alpha_color_texture_);
}
// Actualiza la lógica del objeto
void Background::update(float delta_time) {
// Actualiza la progresión y calcula transiciones
if (!manual_mode_) {
updateProgression(delta_time);
}
// Actualiza el valor de alpha
updateAlphaColorTexture(delta_time);
// Actualiza las nubes
updateClouds(delta_time);
// Actualiza el sprite con la hierba
grass_sprite_->update(delta_time);
// Calcula el valor de alpha
alpha_ = std::max((255 - static_cast<int>(255 * transition_)), 0);
// Mueve el sol y la luna según la progresión
sun_sprite_->setPosition(sun_path_.at(sun_index_));
moon_sprite_->setPosition(moon_path_.at(moon_index_));
// Compone todos los elementos del fondo en la textura
fillCanvas();
}
// Incrementa la progresión interna
void Background::incrementProgress(float amount) {
if (state_ == State::NORMAL) {
float old_progress = progress_;
progress_ += amount;
progress_ = std::min(progress_, total_progress_to_complete_);
// Notifica el cambio si hay callback y el progreso cambió
if (progress_callback_ && progress_ != old_progress) {
progress_callback_(progress_);
}
}
}
// Establece la progresión absoluta
void Background::setProgress(float absolute_progress) {
float old_progress = progress_;
progress_ = std::clamp(absolute_progress, 0.0F, total_progress_to_complete_);
// Notifica el cambio si hay callback y el progreso cambió
if (progress_callback_ && progress_ != old_progress) {
progress_callback_(progress_);
}
}
// Cambia el estado del fondo
void Background::setState(State new_state) {
// Si entra en estado completado, inicializar variables de transición
if (new_state == State::COMPLETED && state_ != State::COMPLETED) {
completion_initial_progress_ = progress_;
completion_transition_timer_ = 0.0F;
}
state_ = new_state;
}
// Reinicia la progresión
void Background::reset() {
float old_progress = progress_;
progress_ = 0.0F;
state_ = State::NORMAL;
manual_mode_ = false;
gradient_number_ = 0;
transition_ = 0.0F;
sun_index_ = 0;
moon_index_ = 0;
// Resetear variables de transición de completado
completion_transition_timer_ = 0.0F;
completion_initial_progress_ = 0.0F;
// Notifica el cambio si hay callback
if (progress_callback_ && progress_ != old_progress) {
progress_callback_(progress_);
}
}
// Activa/desactiva el modo manual
void Background::setManualMode(bool manual) {
manual_mode_ = manual;
}
// Establece callback para sincronización automática
void Background::setProgressCallback(ProgressCallback callback) {
progress_callback_ = std::move(callback);
}
// Elimina el callback
void Background::removeProgressCallback() {
progress_callback_ = nullptr;
}
// Ajusta la velocidad de las nubes
void Background::setCloudsSpeed(float value) {
clouds_speed_ = value;
// En modo manual, aplicar la velocidad directamente
// Las nubes inferiores van a la mitad de velocidad que las superiores
top_clouds_sprite_a_->setVelX(value);
top_clouds_sprite_b_->setVelX(value);
bottom_clouds_sprite_a_->setVelX(value / 2.0F);
bottom_clouds_sprite_b_->setVelX(value / 2.0F);
}
// Establece el degradado de fondo
void Background::setGradientNumber(int value) {
gradient_number_ = value % STAGES;
}
// Ajusta la transición entre texturas
void Background::setTransition(float value) {
transition_ = std::clamp(value, 0.0F, 1.0F);
}
// Establece la posición del sol
void Background::setSunProgression(float progress) {
progress = std::clamp(progress, 0.0F, 1.0F);
sun_index_ = static_cast<size_t>(progress * (sun_path_.size() - 1));
}
// Establece la posición de la luna
void Background::setMoonProgression(float progress) {
progress = std::clamp(progress, 0.0F, 1.0F);
moon_index_ = static_cast<size_t>(progress * (moon_path_.size() - 1));
}
// Actualiza la progresión y calcula las transiciones
void Background::updateProgression(float delta_time) {
// Si el juego está completado, hacer transición suave con easing
if (state_ == State::COMPLETED) {
completion_transition_timer_ += delta_time;
// Calcular progreso normalizado de la transición (0.0 a 1.0)
float t = std::min(completion_transition_timer_ / COMPLETION_TRANSITION_DURATION_S, 1.0F);
if (t < 1.0F) {
// Usar easeOutCubic para transición suave (rápido al inicio, lento al final)
float eased_t = easeOutCubic(static_cast<double>(t));
// Interpolación desde progreso inicial hasta mínimo
float progress_range = completion_initial_progress_ - minimum_completed_progress_;
progress_ = completion_initial_progress_ - (progress_range * eased_t);
} else {
// Transición completada, fijar al valor mínimo
progress_ = minimum_completed_progress_;
}
}
// Calcula la transición de los diferentes fondos
const float GRADIENT_NUMBER_FLOAT = std::min(progress_ / progress_per_stage_, 3.0F);
const float PERCENT = GRADIENT_NUMBER_FLOAT - static_cast<int>(GRADIENT_NUMBER_FLOAT);
gradient_number_ = static_cast<size_t>(GRADIENT_NUMBER_FLOAT);
transition_ = PERCENT;
// Calcula la posición del sol
const float SUN_PROGRESSION = std::min(progress_ / sun_completion_progress_, 1.0F);
sun_index_ = static_cast<size_t>(SUN_PROGRESSION * (sun_path_.size() - 1));
// Calcula la posición de la luna
const float MOON_PROGRESSION = std::min(progress_ / total_progress_to_complete_, 1.0F);
moon_index_ = static_cast<size_t>(MOON_PROGRESSION * (moon_path_.size() - 1));
// Actualiza la velocidad de las nubes
updateCloudsSpeed();
}
// Actualiza la velocidad de las nubes según el estado y progresión
void Background::updateCloudsSpeed() {
// Cálculo de velocidad según progreso (convertido de frame-based a time-based)
constexpr float CLOUDS_INITIAL_SPEED_PX_PER_S = 0.05F * 60.0F; // 3.0 píxeles/segundo (era 0.05 px/frame @ 60fps)
constexpr float CLOUDS_TOTAL_SPEED_PX_PER_S = 2.00F * 60.0F; // 120.0 píxeles/segundo (era 2.00 px/frame @ 60fps)
constexpr float CLOUDS_FINAL_SPEED_RANGE_PX_PER_S = CLOUDS_TOTAL_SPEED_PX_PER_S - CLOUDS_INITIAL_SPEED_PX_PER_S; // 117.0 píxeles/segundo
// Velocidad base según progreso (de -3.0 a -120.0 píxeles/segundo, igual que la versión original)
float base_clouds_speed = (-CLOUDS_INITIAL_SPEED_PX_PER_S) +
(-CLOUDS_FINAL_SPEED_RANGE_PX_PER_S * (progress_ / total_progress_to_complete_));
// En estado completado, las nubes se ralentizan gradualmente
if (state_ == State::COMPLETED) {
float completion_factor = (progress_ - minimum_completed_progress_) /
(total_progress_to_complete_ - minimum_completed_progress_);
completion_factor = std::max(0.1F, completion_factor);
base_clouds_speed *= completion_factor;
}
// Aplicar velocidades diferentes para nubes superiores e inferiores
const float TOP_CLOUDS_SPEED = base_clouds_speed;
const float BOTTOM_CLOUDS_SPEED = base_clouds_speed / 2.0F;
// Aplicar las velocidades a los sprites correspondientes
top_clouds_sprite_a_->setVelX(TOP_CLOUDS_SPEED);
top_clouds_sprite_b_->setVelX(TOP_CLOUDS_SPEED);
bottom_clouds_sprite_a_->setVelX(BOTTOM_CLOUDS_SPEED);
bottom_clouds_sprite_b_->setVelX(BOTTOM_CLOUDS_SPEED);
// Guardar la velocidad principal
clouds_speed_ = TOP_CLOUDS_SPEED;
}
// Actualiza las nubes
void Background::updateClouds(float delta_time) {
// Mueve las nubes
top_clouds_sprite_a_->update(delta_time);
top_clouds_sprite_b_->update(delta_time);
bottom_clouds_sprite_a_->update(delta_time);
bottom_clouds_sprite_b_->update(delta_time);
// Calcula el offset de las nubes
if (top_clouds_sprite_a_->getPosX() < -top_clouds_sprite_a_->getWidth()) {
top_clouds_sprite_a_->setPosX(top_clouds_sprite_a_->getWidth());
}
if (top_clouds_sprite_b_->getPosX() < -top_clouds_sprite_b_->getWidth()) {
top_clouds_sprite_b_->setPosX(top_clouds_sprite_b_->getWidth());
}
if (bottom_clouds_sprite_a_->getPosX() < -bottom_clouds_sprite_a_->getWidth()) {
bottom_clouds_sprite_a_->setPosX(bottom_clouds_sprite_a_->getWidth());
}
if (bottom_clouds_sprite_b_->getPosX() < -bottom_clouds_sprite_b_->getWidth()) {
bottom_clouds_sprite_b_->setPosX(bottom_clouds_sprite_b_->getWidth());
}
}
// Dibuja el gradiente de fondo
void Background::renderGradient() {
// Dibuja el gradiente de detras
gradients_texture_->setAlpha(255);
gradient_sprite_->setSpriteClip(gradient_rect_[(gradient_number_ + 1) % 4]);
gradient_sprite_->render();
// Dibuja el gradiente de delante con una opacidad cada vez menor
gradients_texture_->setAlpha(alpha_);
gradient_sprite_->setSpriteClip(gradient_rect_[gradient_number_]);
gradient_sprite_->render();
}
// Dibuja las nubes de arriba
void Background::renderTopClouds() {
// Dibuja el primer conjunto de nubes, las de detras
top_clouds_texture_->setAlpha(255);
top_clouds_sprite_a_->setSpriteClip(top_clouds_rect_[(gradient_number_ + 1) % 4]);
top_clouds_sprite_b_->setSpriteClip(top_clouds_rect_[(gradient_number_ + 1) % 4]);
top_clouds_sprite_a_->render();
top_clouds_sprite_b_->render();
// Dibuja el segundo conjunto de nubes, las de delante
top_clouds_texture_->setAlpha(alpha_);
top_clouds_sprite_a_->setSpriteClip(top_clouds_rect_[gradient_number_]);
top_clouds_sprite_b_->setSpriteClip(top_clouds_rect_[gradient_number_]);
top_clouds_sprite_a_->render();
top_clouds_sprite_b_->render();
}
// Dibuja las nubes de abajo
void Background::renderBottomClouds() {
// Dibuja el primer conjunto de nubes, las de detras
bottom_clouds_texture_->setAlpha(255);
bottom_clouds_sprite_a_->setSpriteClip(bottom_clouds_rect_[(gradient_number_ + 1) % 4]);
bottom_clouds_sprite_b_->setSpriteClip(bottom_clouds_rect_[(gradient_number_ + 1) % 4]);
bottom_clouds_sprite_a_->render();
bottom_clouds_sprite_b_->render();
// Dibuja el segundo conjunto de nubes, las de delante
bottom_clouds_texture_->setAlpha(alpha_);
bottom_clouds_sprite_a_->setSpriteClip(bottom_clouds_rect_[gradient_number_]);
bottom_clouds_sprite_b_->setSpriteClip(bottom_clouds_rect_[gradient_number_]);
bottom_clouds_sprite_a_->render();
bottom_clouds_sprite_b_->render();
}
// Compone todos los elementos del fondo en la textura
void Background::fillCanvas() {
// Cambia el destino del renderizador
auto* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, canvas_);
// Dibuja el gradiente de fondo
renderGradient();
// Dibuja los astros
sun_sprite_->render();
moon_sprite_->render();
// Dibuja las nubes de arriba
renderTopClouds();
// Dibuja las nubes de abajo
renderBottomClouds();
// Dibuja los edificios
buildings_sprite_->render();
// Dibuja la hierba
grass_sprite_->render();
// Deja el renderizador apuntando donde estaba
SDL_SetRenderTarget(renderer_, temp);
}
// Dibuja el objeto
void Background::render() {
// Fondo
SDL_RenderTexture(renderer_, canvas_, &src_rect_, &dst_rect_);
// Atenuación
SDL_RenderTexture(renderer_, color_texture_, &src_rect_, &dst_rect_);
}
// Establece la posición del objeto
void Background::setPos(SDL_FRect pos) {
dst_rect_ = pos;
// Si cambian las medidas del destino, hay que cambiar las del origen para evitar deformar la imagen
src_rect_.x = 0;
src_rect_.y = rect_.h - pos.h;
src_rect_.w = pos.w;
src_rect_.h = pos.h;
}
// Establece el color de atenuación
void Background::setColor(Color color) {
attenuate_color_ = color;
// Colorea la textura
auto* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, color_texture_);
SDL_SetRenderDrawColor(renderer_, attenuate_color_.r, attenuate_color_.g, attenuate_color_.b, 255);
SDL_RenderClear(renderer_);
SDL_SetRenderTarget(renderer_, temp);
}
// Establece la transparencia de la atenuación
void Background::setAlpha(int alpha) {
// Evita que se asignen valores fuera de rango
alpha = std::clamp(alpha, 0, 255);
// Guarda el valor actual y establece el nuevo valor
previous_alpha_color_texture_ = alpha_color_texture_;
alpha_color_texture_ = alpha;
}
// Actualiza el valor de alpha (time-based)
void Background::updateAlphaColorTexture(float delta_time) {
// 1. Si ya hemos llegado al destino, no hacemos nada.
if (alpha_color_texture_ == previous_alpha_color_texture_) {
return;
}
// 2. Define la velocidad del cambio (p. ej., 150 unidades de alfa por segundo).
// Puedes ajustar este valor para que la transición sea más rápida o lenta.
constexpr float ALPHA_TRANSITION_SPEED = 150.0F;
// 3. Determina la dirección del cambio (subir o bajar el alfa)
if (alpha_color_texture_ > previous_alpha_color_texture_) {
// Aumentar el alfa
current_alpha_float_ += ALPHA_TRANSITION_SPEED * delta_time;
// Nos aseguramos de no pasarnos del objetivo
current_alpha_float_ = std::min(current_alpha_float_, static_cast<float>(alpha_color_texture_));
} else {
// Disminuir el alfa
current_alpha_float_ -= ALPHA_TRANSITION_SPEED * delta_time;
// Nos aseguramos de no quedarnos cortos del objetivo
current_alpha_float_ = std::max(current_alpha_float_, static_cast<float>(alpha_color_texture_));
}
// 4. Actualiza el valor entero solo si ha cambiado lo suficiente
// Usamos std::round para un redondeo más natural.
const auto NEW_ALPHA = static_cast<size_t>(std::round(current_alpha_float_));
if (NEW_ALPHA != previous_alpha_color_texture_) {
previous_alpha_color_texture_ = NEW_ALPHA;
// SDL espera un Uint8 (0-255), así que hacemos un cast seguro.
SDL_SetTextureAlphaMod(color_texture_, static_cast<Uint8>(previous_alpha_color_texture_));
}
}
// Precalcula el vector con el recorrido del sol
void Background::createSunPath() {
constexpr float CENTER_X = 170;
const float CENTER_Y = base_ - 80;
constexpr float RADIUS = 120;
// Generar puntos de la curva desde 90 a 180 grados
constexpr double STEP = 0.01;
const int NUM_STEPS = static_cast<int>((M_PI - M_PI / 2) / STEP) + 1;
for (int i = 0; i < NUM_STEPS; ++i) {
double theta = M_PI / 2 + (i * STEP);
float x = CENTER_X + (RADIUS * cos(theta));
float y = CENTER_Y - (RADIUS * sin(theta));
sun_path_.push_back({.x = x, .y = y});
}
// Agregar puntos en línea recta después de la curva
constexpr int EXTRA_PIXELS = 40;
SDL_FPoint last_point = sun_path_.back();
for (int i = 1; i <= EXTRA_PIXELS; ++i) {
sun_path_.push_back({.x = last_point.x, .y = last_point.y + i});
}
}
// Precalcula el vector con el recorrido de la luna
void Background::createMoonPath() {
constexpr float CENTER_X = 100;
const float CENTER_Y = base_ - 50;
constexpr float RADIUS = 140;
constexpr double STEP = 0.01;
const int NUM_STEPS = static_cast<int>((M_PI / 2) / STEP) + 1;
constexpr float FREEZE_PERCENTAGE = 0.2F; // Porcentaje final del recorrido que se mantiene fijo
const int FREEZE_START_INDEX = static_cast<int>(NUM_STEPS * (1.0F - FREEZE_PERCENTAGE));
for (int i = 0; i < NUM_STEPS; ++i) {
double theta = i * STEP;
float x = CENTER_X + (RADIUS * cos(theta));
float y = CENTER_Y - (RADIUS * sin(theta));
if (i >= FREEZE_START_INDEX && !moon_path_.empty()) {
moon_path_.push_back(moon_path_.back()); // Repite el último punto válido
} else {
moon_path_.push_back({.x = x, .y = y});
}
}
}

View File

@@ -0,0 +1,140 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect, SDL_FPoint, SDL_Texture, SDL_Renderer
#include <array> // Para array
#include <cstddef> // Para size_t
#include <functional> // Para function
#include <memory> // Para unique_ptr, shared_ptr
#include <vector> // Para vector
#include "color.hpp" // Para Color
class MovingSprite;
class Sprite;
class Texture;
class AnimatedSprite;
// --- Clase Background: gestiona el fondo de la sección jugable ---
class Background {
public:
// --- Enums ---
enum class State {
NORMAL, // Progresión normal del día
COMPLETED // Reducción gradual de la actividad
};
// --- Tipos ---
using ProgressCallback = std::function<void(float)>; // Callback para sincronización
// --- Constructor y destructor ---
Background(float total_progress_to_complete = 6100.0F); // Constructor principal
~Background(); // Destructor
// --- Métodos principales ---
void update(float delta_time); // Actualiza la lógica del objeto
void render(); // Dibuja el objeto
void reset(); // Reinicia la progresión
// --- Configuración ---
void setPos(SDL_FRect pos); // Establece la posición del objeto
void setState(State new_state); // Cambia el estado del fondo
void setProgressCallback(ProgressCallback callback); // Establece callback para sincronización
void removeProgressCallback(); // Elimina el callback
void setManualMode(bool manual); // Activa/desactiva el modo manual
void setCloudsSpeed(float value); // Ajusta la velocidad de las nubes
void setGradientNumber(int value); // Establece el degradado de fondo
void setTransition(float value); // Ajusta la transición entre texturas
void setSunProgression(float progress); // Establece la posición del sol
void setMoonProgression(float progress); // Establece la posición de la luna
void setColor(Color color); // Establece el color de atenuación
void setAlpha(int alpha); // Ajusta la transparencia del fondo
// --- Control de progresión ---
void incrementProgress(float amount = 1.0F); // Incrementa la progresión interna
void setProgress(float absolute_progress); // Establece la progresión absoluta
// --- Getters ---
[[nodiscard]] auto getProgress() const -> float { return progress_; } // Obtiene el progreso actual
[[nodiscard]] auto getState() const -> State { return state_; } // Obtiene el estado actual
[[nodiscard]] auto getCurrentGradient() const -> int { return static_cast<int>(gradient_number_); } // Obtiene el gradiente actual
private:
// --- Constantes ---
static constexpr size_t STAGES = 4; // Número de etapas
static constexpr float MINIMUM_COMPLETED_PROGRESS_PERCENTAGE = 0.05F; // Porcentaje mínimo completado (10%)
static constexpr float SUN_COMPLETION_FACTOR = 0.5F; // Factor de completado del sol
static constexpr float COMPLETION_TRANSITION_DURATION_S = 3.0F; // Duración de la transición de completado en segundos
// --- Objetos y punteros ---
SDL_Renderer* renderer_; // Renderizador de la ventana
SDL_Texture* canvas_; // Textura para componer el fondo
SDL_Texture* color_texture_; // Textura para atenuar el fondo
std::shared_ptr<Texture> buildings_texture_; // Textura de edificios
std::shared_ptr<Texture> top_clouds_texture_; // Textura de nubes superiores
std::shared_ptr<Texture> bottom_clouds_texture_; // Textura de nubes inferiores
std::shared_ptr<Texture> gradients_texture_; // Textura de gradientes
std::shared_ptr<Texture> sun_texture_; // Textura del sol
std::shared_ptr<Texture> moon_texture_; // Textura de la luna
std::unique_ptr<MovingSprite> top_clouds_sprite_a_; // Sprite de nubes superiores A
std::unique_ptr<MovingSprite> top_clouds_sprite_b_; // Sprite de nubes superiores B
std::unique_ptr<MovingSprite> bottom_clouds_sprite_a_; // Sprite de nubes inferiores A
std::unique_ptr<MovingSprite> bottom_clouds_sprite_b_; // Sprite de nubes inferiores B
std::unique_ptr<Sprite> buildings_sprite_; // Sprite de edificios
std::unique_ptr<Sprite> gradient_sprite_; // Sprite de gradiente
std::unique_ptr<Sprite> sun_sprite_; // Sprite del sol
std::unique_ptr<Sprite> moon_sprite_; // Sprite de la luna
std::unique_ptr<AnimatedSprite> grass_sprite_; // Sprite con la hierba
// --- Variables de configuración ---
const float total_progress_to_complete_; // Progreso total para completar
const float progress_per_stage_; // Progreso por etapa
const float sun_completion_progress_; // Progreso de completado del sol
const float minimum_completed_progress_; // Progreso mínimo calculado dinámicamente
ProgressCallback progress_callback_; // Callback para notificar cambios de progreso
// --- Variables de estado ---
std::vector<SDL_FPoint> sun_path_; // Recorrido del sol
std::vector<SDL_FPoint> moon_path_; // Recorrido de la luna
std::array<SDL_FRect, STAGES> gradient_rect_; // Fondos degradados
std::array<SDL_FRect, 4> top_clouds_rect_; // Nubes superiores
std::array<SDL_FRect, 4> bottom_clouds_rect_; // Nubes inferiores
SDL_FRect rect_; // Tamaño del objeto
SDL_FRect src_rect_; // Parte del objeto para copiar en pantalla
SDL_FRect dst_rect_; // Posición en pantalla donde se copia el objeto
Color attenuate_color_; // Color de atenuación
State state_ = State::NORMAL; // Estado actual
float progress_ = 0.0F; // Progresión interna
float clouds_speed_ = 0; // Velocidad de las nubes
float transition_ = 0; // Porcentaje de transición
float current_alpha_float_ = 0.0F; // Acumulador para el valor alfa preciso
size_t gradient_number_ = 0; // Índice de fondo degradado
size_t alpha_color_texture_ = 0; // Transparencia de atenuación
size_t previous_alpha_color_texture_ = 0; // Transparencia anterior
size_t sun_index_ = 0; // Índice del recorrido del sol
size_t moon_index_ = 0; // Índice del recorrido de la luna
int base_ = 0; // Posición base del fondo
Uint8 alpha_ = 0; // Transparencia entre fases
bool manual_mode_ = false; // Si está en modo manual
// --- Variables para transición suave de completado ---
float completion_transition_timer_ = 0.0F; // Timer para la transición de completado
float completion_initial_progress_ = 0.0F; // Progreso inicial al entrar en estado completado
// --- Métodos internos ---
void initializePaths(); // Inicializa las rutas del sol y la luna
void initializeRects(); // Inicializa los rectángulos de gradientes y nubes
void initializeSprites(); // Crea los sprites
void initializeSpriteProperties(); // Configura las propiedades iniciales de los sprites
void initializeTextures(); // Inicializa las texturas de renderizado
void updateProgression(float delta_time); // Actualiza la progresión y calcula transiciones
void updateCloudsSpeed(); // Actualiza la velocidad de las nubes según el estado
void renderGradient(); // Dibuja el gradiente de fondo
void renderTopClouds(); // Dibuja las nubes superiores
void renderBottomClouds(); // Dibuja las nubes inferiores
void fillCanvas(); // Compone todos los elementos en la textura
void updateAlphaColorTexture(float delta_time); // Actualiza el alpha de la textura de atenuación
void updateClouds(float delta_time); // Actualiza el movimiento de las nubes (time-based)
void createSunPath(); // Precalcula el recorrido del sol
void createMoonPath(); // Precalcula el recorrido de la luna
};

View File

@@ -0,0 +1,551 @@
#include "fade.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cstdlib>
#include <utility>
#include "color.hpp"
#include "param.hpp"
#include "screen.hpp"
// Constructor
Fade::Fade()
: renderer_(Screen::get()->getRenderer()) {
// Crea la textura donde dibujar el fade
backbuffer_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);
SDL_SetTextureBlendMode(backbuffer_, SDL_BLENDMODE_BLEND);
// Inicializa las variables
init();
}
// Destructor
Fade::~Fade() {
SDL_DestroyTexture(backbuffer_);
}
// Inicializa las variables
void Fade::init() {
type_ = Type::CENTER;
mode_ = Mode::OUT;
r_ = 0;
g_ = 0;
b_ = 0;
a_ = 0;
post_duration_ = 0;
post_start_time_ = 0;
pre_duration_ = 0;
pre_start_time_ = 0;
fading_duration_ = param.fade.random_squares_duration_ms; // Duración por defecto para FADING
fading_start_time_ = 0;
num_squares_width_ = param.fade.num_squares_width;
num_squares_height_ = param.fade.num_squares_height;
square_transition_duration_ = fading_duration_ / 4; // 25% del tiempo total para la transición individual
}
// Resetea algunas variables para volver a hacer el fade sin perder ciertos parametros
void Fade::reset() {
state_ = State::NOT_ENABLED;
post_start_time_ = 0;
pre_start_time_ = 0;
fading_start_time_ = 0;
value_ = 0;
// La duración del fade se mantiene, se puede cambiar con setDuration()
}
// Pinta una transición en pantalla
void Fade::render() {
if (state_ != State::NOT_ENABLED) {
// Para fade IN terminado, no renderizar (auto-desactivación visual)
if (state_ == State::FINISHED && mode_ == Mode::IN) {
return;
}
SDL_RenderTexture(renderer_, backbuffer_, nullptr, nullptr);
}
}
// Actualiza las variables internas
void Fade::update(float delta_time) {
switch (state_) {
case State::PRE:
updatePreState();
break;
case State::FADING:
updateFadingState();
break;
case State::POST:
updatePostState();
break;
default:
break;
}
}
void Fade::updatePreState() {
Uint32 elapsed_time = SDL_GetTicks() - pre_start_time_;
if (std::cmp_greater_equal(elapsed_time, pre_duration_)) {
state_ = State::FADING;
fading_start_time_ = SDL_GetTicks(); // Inicia el temporizador del fade AQUI
}
}
void Fade::updateFadingState() {
switch (type_) {
case Type::FULLSCREEN:
updateFullscreenFade();
break;
case Type::CENTER:
updateCenterFade();
break;
case Type::RANDOM_SQUARE:
updateRandomSquareFade();
break;
case Type::RANDOM_SQUARE2:
updateRandomSquare2Fade();
break;
case Type::DIAGONAL:
updateDiagonalFade();
break;
case Type::VENETIAN:
updateVenetianFade();
break;
default:
break;
}
}
void Fade::changeToPostState() {
state_ = State::POST;
post_start_time_ = SDL_GetTicks();
}
void Fade::updatePostState() {
Uint32 elapsed_time = SDL_GetTicks() - post_start_time_;
if (std::cmp_greater_equal(elapsed_time, post_duration_)) {
state_ = State::FINISHED;
}
// Mantener el estado final del fade
Uint8 post_alpha = (mode_ == Mode::OUT) ? 255 : 0;
cleanBackbuffer(r_, g_, b_, post_alpha);
}
void Fade::updateFullscreenFade() {
Uint32 elapsed_time = SDL_GetTicks() - fading_start_time_;
float progress = std::min(static_cast<float>(elapsed_time) / fading_duration_, 1.0F);
// Modifica la transparencia basada en el progreso
auto current_alpha = static_cast<Uint8>(progress * 255.0F);
a_ = (mode_ == Mode::OUT) ? current_alpha : 255 - current_alpha;
SDL_SetTextureAlphaMod(backbuffer_, a_);
value_ = static_cast<int>(progress * 100);
// Comprueba si ha terminado
if (std::cmp_greater_equal(elapsed_time, fading_duration_)) {
changeToPostState();
}
}
void Fade::updateCenterFade() {
Uint32 elapsed_time = SDL_GetTicks() - fading_start_time_;
float progress = std::min(static_cast<float>(elapsed_time) / fading_duration_, 1.0F);
// Calcula la altura de las barras
float rect_height = progress * (param.game.height / 2.0F);
if (mode_ == Mode::IN) {
rect_height = (param.game.height / 2.0F) - rect_height;
}
rect1_.h = rect_height;
rect2_.h = rect_height;
rect2_.y = param.game.height - rect_height;
drawCenterFadeRectangles();
value_ = static_cast<int>(progress * 100);
// Comprueba si ha terminado
if (std::cmp_greater_equal(elapsed_time, fading_duration_)) {
a_ = (mode_ == Mode::OUT) ? 255 : 0;
changeToPostState();
}
}
void Fade::drawCenterFadeRectangles() {
auto* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, backbuffer_);
cleanBackbuffer(r_, g_, b_, 0); // Limpiar para modo IN
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, 255);
SDL_RenderFillRect(renderer_, &rect1_);
SDL_RenderFillRect(renderer_, &rect2_);
SDL_SetRenderTarget(renderer_, temp);
}
void Fade::updateRandomSquareFade() {
Uint32 elapsed_time = SDL_GetTicks() - fading_start_time_;
float progress = std::min(static_cast<float>(elapsed_time) / fading_duration_, 1.0F);
// Calcula cuántos cuadrados deberían estar activos
int total_squares = num_squares_width_ * num_squares_height_;
int active_squares = static_cast<int>(progress * total_squares);
drawRandomSquares(active_squares);
value_ = static_cast<int>(progress * 100);
// Comprueba si ha terminado
if (std::cmp_greater_equal(elapsed_time, fading_duration_)) {
changeToPostState();
}
}
void Fade::updateRandomSquare2Fade() {
Uint32 elapsed_time = SDL_GetTicks() - fading_start_time_;
int total_squares = num_squares_width_ * num_squares_height_;
int activation_time = fading_duration_ - square_transition_duration_;
activation_time = std::max(activation_time, square_transition_duration_);
int squares_to_activate = 0;
if (mode_ == Mode::OUT) {
if (std::cmp_less(elapsed_time, activation_time)) {
float activation_progress = static_cast<float>(elapsed_time) / activation_time;
squares_to_activate = static_cast<int>(activation_progress * total_squares);
} else {
squares_to_activate = total_squares;
}
for (int i = 0; i < squares_to_activate; ++i) {
if (square_age_[i] == -1) { square_age_[i] = elapsed_time; }
}
} else {
squares_to_activate = total_squares;
float activation_progress = static_cast<float>(elapsed_time) / activation_time;
int squares_starting_transition = std::min(total_squares, std::max(1, static_cast<int>(activation_progress * total_squares)));
for (int i = 0; i < squares_starting_transition; ++i) {
if (square_age_[i] == -1) { square_age_[i] = elapsed_time; }
}
}
drawRandomSquares2();
value_ = calculateValue(0, total_squares, squares_to_activate);
if (std::cmp_greater_equal(elapsed_time, fading_duration_)) {
Uint8 final_alpha = (mode_ == Mode::OUT) ? 255 : 0;
cleanBackbuffer(r_, g_, b_, final_alpha);
changeToPostState();
}
}
void Fade::updateDiagonalFade() {
Uint32 elapsed_time = SDL_GetTicks() - fading_start_time_;
int activation_time = fading_duration_ - square_transition_duration_;
activation_time = std::max(activation_time, square_transition_duration_);
int max_diagonal = num_squares_width_ + num_squares_height_ - 1;
int active_diagonals = 0;
if (mode_ == Mode::OUT) {
if (std::cmp_less(elapsed_time, activation_time)) {
float activation_progress = static_cast<float>(elapsed_time) / activation_time;
active_diagonals = static_cast<int>(activation_progress * max_diagonal);
} else {
active_diagonals = max_diagonal;
}
for (int diagonal = 0; diagonal < active_diagonals; ++diagonal) {
activateDiagonal(diagonal, elapsed_time);
}
} else {
active_diagonals = max_diagonal;
if (std::cmp_less(elapsed_time, activation_time)) {
float activation_progress = static_cast<float>(elapsed_time) / activation_time;
int diagonals_starting_transition = static_cast<int>(activation_progress * max_diagonal);
for (int diagonal = 0; diagonal < diagonals_starting_transition; ++diagonal) {
activateDiagonal(diagonal, elapsed_time);
}
} else {
for (int diagonal = 0; diagonal < max_diagonal; ++diagonal) {
activateDiagonal(diagonal, elapsed_time);
}
}
}
drawDiagonal();
value_ = calculateValue(0, max_diagonal, active_diagonals);
if (std::cmp_greater_equal(elapsed_time, fading_duration_)) {
Uint8 final_alpha = (mode_ == Mode::OUT) ? 255 : 0;
cleanBackbuffer(r_, g_, b_, final_alpha);
changeToPostState();
}
}
void Fade::activateDiagonal(int diagonal_index, Uint32 current_time) {
for (int x = 0; x < num_squares_width_; ++x) {
int y = diagonal_index - x;
if (y >= 0 && y < num_squares_height_) {
int index = (y * num_squares_width_) + x;
if (index >= 0 && std::cmp_less(index, square_age_.size()) && square_age_[index] == -1) {
square_age_[index] = current_time;
}
}
}
}
void Fade::drawDiagonal() {
auto* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, backbuffer_);
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0);
SDL_RenderClear(renderer_);
SDL_BlendMode blend_mode;
SDL_GetRenderDrawBlendMode(renderer_, &blend_mode);
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
Uint32 current_time = SDL_GetTicks() - fading_start_time_;
for (size_t i = 0; i < square_.size(); ++i) {
Uint8 current_alpha = 0;
if (square_age_[i] == -1) {
current_alpha = (mode_ == Mode::OUT) ? 0 : a_;
} else {
Uint32 square_elapsed = current_time - square_age_[i];
float progress = std::min(static_cast<float>(square_elapsed) / square_transition_duration_, 1.0F);
current_alpha = (mode_ == Mode::OUT) ? static_cast<Uint8>(progress * a_) : static_cast<Uint8>((1.0F - progress) * a_);
}
if (current_alpha > 0) {
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, current_alpha);
SDL_RenderFillRect(renderer_, &square_[i]);
}
}
SDL_SetRenderDrawBlendMode(renderer_, blend_mode);
SDL_SetRenderTarget(renderer_, temp);
}
void Fade::drawRandomSquares(int active_count) {
auto* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, backbuffer_);
// El fondo se prepara en activate()
SDL_BlendMode blend_mode;
SDL_GetRenderDrawBlendMode(renderer_, &blend_mode);
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_NONE);
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, a_);
// Dibuja solo los cuadrados activos
for (int i = 0; i < active_count && std::cmp_less(i, square_.size()); ++i) {
SDL_RenderFillRect(renderer_, &square_[i]);
}
SDL_SetRenderDrawBlendMode(renderer_, blend_mode);
SDL_SetRenderTarget(renderer_, temp);
}
void Fade::drawRandomSquares2() {
auto* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, backbuffer_);
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0);
SDL_RenderClear(renderer_);
SDL_BlendMode blend_mode;
SDL_GetRenderDrawBlendMode(renderer_, &blend_mode);
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
Uint32 current_time = SDL_GetTicks() - fading_start_time_;
for (size_t i = 0; i < square_.size(); ++i) {
Uint8 current_alpha = 0;
if (square_age_[i] == -1) {
current_alpha = (mode_ == Mode::OUT) ? 0 : a_;
} else {
Uint32 square_elapsed = current_time - square_age_[i];
float progress = std::min(static_cast<float>(square_elapsed) / square_transition_duration_, 1.0F);
current_alpha = (mode_ == Mode::OUT) ? static_cast<Uint8>(progress * a_) : static_cast<Uint8>((1.0F - progress) * a_);
}
if (current_alpha > 0) {
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, current_alpha);
SDL_RenderFillRect(renderer_, &square_[i]);
}
}
SDL_SetRenderDrawBlendMode(renderer_, blend_mode);
SDL_SetRenderTarget(renderer_, temp);
}
void Fade::updateVenetianFade() {
Uint32 elapsed_time = SDL_GetTicks() - fading_start_time_;
float progress = std::min(static_cast<float>(elapsed_time) / fading_duration_, 1.0F);
// Calcula la altura de las persianas
float rect_height = progress * param.fade.venetian_size;
if (mode_ == Mode::IN) {
rect_height = param.fade.venetian_size - rect_height;
}
for (auto& rect : square_) {
rect.h = rect_height;
}
drawVenetianBlinds();
value_ = static_cast<int>(progress * 100);
// Comprueba si ha terminado
if (std::cmp_greater_equal(elapsed_time, fading_duration_)) {
changeToPostState();
}
}
void Fade::drawVenetianBlinds() {
auto* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, backbuffer_);
// Limpia la textura con el color base (transparente para OUT, opaco para IN)
Uint8 initial_alpha = (mode_ == Mode::OUT) ? 0 : 255;
cleanBackbuffer(r_, g_, b_, initial_alpha);
SDL_BlendMode blend_mode;
SDL_GetRenderDrawBlendMode(renderer_, &blend_mode);
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_NONE);
// Dibuja las persianas con el color opuesto al fondo
Uint8 draw_alpha = (mode_ == Mode::OUT) ? 255 : 0;
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, draw_alpha);
for (const auto& rect : square_) {
SDL_RenderFillRect(renderer_, &rect);
}
SDL_SetRenderDrawBlendMode(renderer_, blend_mode);
SDL_SetRenderTarget(renderer_, temp);
}
// Activa el fade
void Fade::activate() {
if (state_ != State::NOT_ENABLED) {
return;
}
state_ = State::PRE;
pre_start_time_ = SDL_GetTicks();
value_ = 0;
// Preparación inicial de cada tipo
switch (type_) {
/*case Type::FULLSCREEN:
cleanBackbuffer(r_, g_, b_, (mode_ == Mode::OUT) ? 0 : 255);
SDL_SetTextureAlphaMod(backbuffer_, (mode_ == Mode::OUT) ? 255 : 0);
break;*/
case Type::FULLSCREEN: {
// La textura en sí siempre debe ser de un color sólido y opaco.
// La transparencia se gestionará con la modulación de alfa.
cleanBackbuffer(r_, g_, b_, 255);
// Ahora, inicializamos la modulación de alfa correctamente:
// - IN: Empieza opaco (255) y se desvanece a transparente.
// - OUT: Empieza transparente (0) y se desvanece a opaco.
const Uint8 INITIAL_ALPHA = (mode_ == Mode::IN) ? 255 : 0;
SDL_SetTextureAlphaMod(backbuffer_, INITIAL_ALPHA);
break;
}
case Type::CENTER:
rect1_ = {.x = 0, .y = 0, .w = param.game.width, .h = 0};
rect2_ = {.x = 0, .y = param.game.height, .w = param.game.width, .h = 0};
a_ = 255;
break;
case Type::RANDOM_SQUARE: {
rect1_ = {.x = 0, .y = 0, .w = static_cast<float>(param.game.width / num_squares_width_), .h = static_cast<float>(param.game.height / num_squares_height_)};
square_.clear();
for (int i = 0; i < num_squares_width_ * num_squares_height_; ++i) {
rect1_.x = (i % num_squares_width_) * rect1_.w;
rect1_.y = (i / num_squares_width_) * rect1_.h;
square_.push_back(rect1_);
}
auto num = square_.size();
while (num > 1) {
auto num_arreu = rand() % num;
std::swap(square_[num_arreu], square_[--num]);
}
a_ = (mode_ == Mode::OUT) ? 255 : 0;
cleanBackbuffer(r_, g_, b_, (mode_ == Mode::OUT) ? 0 : 255);
break;
}
case Type::RANDOM_SQUARE2:
case Type::DIAGONAL: {
rect1_ = {.x = 0, .y = 0, .w = static_cast<float>(param.game.width / num_squares_width_), .h = static_cast<float>(param.game.height / num_squares_height_)};
square_.clear();
square_age_.assign(num_squares_width_ * num_squares_height_, -1);
for (int i = 0; i < num_squares_width_ * num_squares_height_; ++i) {
rect1_.x = (i % num_squares_width_) * rect1_.w;
rect1_.y = (i / num_squares_width_) * rect1_.h;
square_.push_back(rect1_);
}
if (type_ == Type::RANDOM_SQUARE2) {
auto num = square_.size();
while (num > 1) {
auto num_arreu = rand() % num;
std::swap(square_[num_arreu], square_[--num]);
// No es necesario desordenar square_age_ ya que todos son -1
}
}
Uint8 initial_alpha = (mode_ == Mode::OUT) ? 0 : 255;
cleanBackbuffer(r_, g_, b_, initial_alpha);
a_ = 255;
square_transition_duration_ = std::max(fading_duration_ / 4, 100);
break;
}
case Type::VENETIAN: {
square_.clear();
rect1_ = {.x = 0, .y = 0, .w = param.game.width, .h = (mode_ == Mode::OUT) ? 0.0F : param.fade.venetian_size};
const int MAX = param.game.height / param.fade.venetian_size;
for (int i = 0; i < MAX; ++i) {
rect1_.y = i * param.fade.venetian_size;
square_.push_back(rect1_);
}
break;
}
}
}
// Establece el color del fade
void Fade::setColor(Uint8 r, Uint8 g, Uint8 b) {
r_ = r;
g_ = g;
b_ = b;
}
// Establece el color del fade
void Fade::setColor(Color color) {
r_ = color.r;
g_ = color.g;
b_ = color.b;
}
// Limpia el backbuffer
void Fade::cleanBackbuffer(Uint8 r, Uint8 g, Uint8 b, Uint8 a) {
auto* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, backbuffer_);
SDL_SetRenderDrawColor(renderer_, r, g, b, a);
SDL_RenderClear(renderer_);
SDL_SetRenderTarget(renderer_, temp);
}
// Calcula el valor del estado del fade
auto Fade::calculateValue(int min, int max, int current) -> int {
if (current <= min) {
return 0;
}
if (current >= max) {
return 100;
}
return static_cast<int>(100.0 * (current - min) / (max - min));
}

View File

@@ -0,0 +1,111 @@
#pragma once
#include <SDL3/SDL.h> // Para Uint8, SDL_FRect, SDL_Renderer, SDL_Texture, Uint32
#include <vector> // Para vector
struct Color;
// --- Clase Fade: gestor de transiciones de fundido ---
class Fade {
public:
// --- Enums ---
enum class Type : Uint8 {
FULLSCREEN = 0, // Fundido de pantalla completa
CENTER = 1, // Fundido desde el centro
RANDOM_SQUARE = 2, // Fundido con cuadrados aleatorios
RANDOM_SQUARE2 = 3, // Fundido con cuadrados aleatorios (variante 2)
DIAGONAL = 4, // Fundido diagonal desde esquina superior izquierda
VENETIAN = 5, // Fundido tipo persiana veneciana
};
enum class Mode : Uint8 {
IN = 0, // Fundido de entrada
OUT = 1, // Fundido de salida
};
enum class State : Uint8 {
NOT_ENABLED = 0, // No activado
PRE = 1, // Estado previo
FADING = 2, // Fundiendo
POST = 3, // Estado posterior
FINISHED = 4, // Finalizado
};
// --- Constructores y destructor ---
Fade();
~Fade();
// --- Métodos principales ---
void reset(); // Resetea variables para reutilizar el fade
void render(); // Dibuja la transición en pantalla
void update(float delta_time = 0.0F); // Actualiza el estado interno
void activate(); // Activa el fade
// --- Configuración ---
void setColor(Uint8 r, Uint8 g, Uint8 b); // Establece el color RGB del fade
void setColor(Color color); // Establece el color del fade
void setType(Type type) { type_ = type; } // Establece el tipo de fade
void setMode(Mode mode) { mode_ = mode; } // Establece el modo de fade
void setDuration(int milliseconds) { fading_duration_ = milliseconds; } // Duración del estado FADING en milisegundos
void setPostDuration(int milliseconds) { post_duration_ = milliseconds; } // Duración posterior al fade en milisegundos
void setPreDuration(int milliseconds) { pre_duration_ = milliseconds; } // Duración previa al fade en milisegundos
// --- Getters ---
[[nodiscard]] auto getValue() const -> int { return value_; }
[[nodiscard]] auto isEnabled() const -> bool { return state_ != State::NOT_ENABLED; }
[[nodiscard]] auto hasEnded() const -> bool { return state_ == State::FINISHED; }
private:
// --- Objetos y punteros ---
SDL_Renderer* renderer_; // Renderizador de la ventana
SDL_Texture* backbuffer_; // Backbuffer para efectos
// --- Variables de estado ---
std::vector<SDL_FRect> square_; // Vector de cuadrados
std::vector<int> square_age_; // Edad de cada cuadrado (para RANDOM_SQUARE2 y DIAGONAL)
SDL_FRect rect1_, rect2_; // Rectángulos para efectos
Type type_; // Tipo de fade
Mode mode_; // Modo de fade
State state_ = State::NOT_ENABLED; // Estado actual
Uint8 r_, g_, b_, a_; // Color del fade (RGBA)
int num_squares_width_; // Cuadrados en horizontal
int num_squares_height_; // Cuadrados en vertical
int square_transition_duration_; // Duración de transición de cada cuadrado en ms
int fading_duration_{0}; // Duración del estado FADING en milisegundos
Uint32 fading_start_time_ = 0; // Tiempo de inicio del estado FADING
int post_duration_ = 0; // Duración posterior en milisegundos
Uint32 post_start_time_ = 0; // Tiempo de inicio del estado POST
int pre_duration_ = 0; // Duración previa en milisegundos
Uint32 pre_start_time_ = 0; // Tiempo de inicio del estado PRE
int value_ = 0; // Estado del fade (0-100)
// --- Inicialización y limpieza ---
void init(); // Inicializa variables
void cleanBackbuffer(Uint8 r, Uint8 g, Uint8 b, Uint8 a); // Limpia el backbuffer con un color RGBA
// --- Utilidades generales ---
static auto calculateValue(int min, int max, int current) -> int; // Calcula el valor del fade entre dos límites
// --- Lógica de estado ---
void updatePreState(); // Actualiza el estado previo al fade
void updateFadingState(); // Actualiza el estado durante el fade
void updatePostState(); // Actualiza el estado posterior al fade
void changeToPostState(); // Cambia al estado POST e inicializa el tiempo
// --- Efectos de fundido (fade) ---
void updateFullscreenFade(); // Actualiza el fundido de pantalla completa
void updateCenterFade(); // Actualiza el fundido desde el centro
void updateRandomSquareFade(); // Actualiza el fundido con cuadrados aleatorios
void updateRandomSquare2Fade(); // Actualiza el fundido con cuadrados aleatorios (variante 2)
void updateDiagonalFade(); // Actualiza el fundido diagonal
void updateVenetianFade(); // Actualiza el fundido tipo persiana veneciana
// --- Dibujo de efectos visuales ---
void drawCenterFadeRectangles(); // Dibuja los rectángulos del fundido central
void drawRandomSquares(int active_count); // Dibuja los cuadrados aleatorios del fundido
void drawRandomSquares2(); // Dibuja los cuadrados con transición de color (RANDOM_SQUARE2)
void drawDiagonal(); // Dibuja los cuadrados con patrón diagonal
void activateDiagonal(int diagonal_index, Uint32 current_time); // Activa una diagonal específica
void drawVenetianBlinds(); // Dibuja las persianas venecianas del fundido
};

View File

@@ -0,0 +1,252 @@
#include "gif.hpp"
#include <SDL3/SDL.h> // Para SDL_LogError, SDL_LogCategory, SDL_LogInfo
#include <cstring> // Para memcpy, size_t
#include <iostream> // Para std::cout
#include <stdexcept> // Para runtime_error
#include <string> // Para char_traits, operator==, basic_string, string
namespace GIF {
inline void readBytes(const uint8_t *&buffer, void *dst, size_t size) {
std::memcpy(dst, buffer, size);
buffer += size;
}
void Gif::decompress(int code_length, const uint8_t *input, int input_length, uint8_t *out) {
if (code_length < 2 || code_length > 12) {
std::cout << "Invalid LZW code length: " << code_length << '\n';
throw std::runtime_error("Invalid LZW code length");
}
int i, bit;
int prev = -1;
std::vector<DictionaryEntry> dictionary;
int dictionary_ind;
unsigned int mask = 0x01;
int reset_code_length = code_length;
int clear_code = 1 << code_length;
int stop_code = clear_code + 1;
int match_len = 0;
dictionary.resize(1 << (code_length + 1));
for (dictionary_ind = 0; dictionary_ind < (1 << code_length); dictionary_ind++) {
dictionary[dictionary_ind].byte = static_cast<uint8_t>(dictionary_ind);
dictionary[dictionary_ind].prev = -1;
dictionary[dictionary_ind].len = 1;
}
dictionary_ind += 2;
while (input_length > 0) {
int code = 0;
for (i = 0; i < (code_length + 1); i++) {
if (input_length <= 0) {
std::cout << "Unexpected end of input in decompress" << '\n';
throw std::runtime_error("Unexpected end of input in decompress");
}
bit = ((*input & mask) != 0) ? 1 : 0;
mask <<= 1;
if (mask == 0x100) {
mask = 0x01;
input++;
input_length--;
}
code |= (bit << i);
}
if (code == clear_code) {
code_length = reset_code_length;
dictionary.resize(1 << (code_length + 1));
for (dictionary_ind = 0; dictionary_ind < (1 << code_length); dictionary_ind++) {
dictionary[dictionary_ind].byte = static_cast<uint8_t>(dictionary_ind);
dictionary[dictionary_ind].prev = -1;
dictionary[dictionary_ind].len = 1;
}
dictionary_ind += 2;
prev = -1;
continue;
} else if (code == stop_code) {
break;
}
if (prev > -1 && code_length < 12) {
if (code > dictionary_ind) {
std::cout << "LZW error: code (" << code << ") exceeds dictionary_ind (" << dictionary_ind << ")" << '\n';
throw std::runtime_error("LZW error: code exceeds dictionary_ind.");
}
int ptr;
if (code == dictionary_ind) {
ptr = prev;
while (dictionary[ptr].prev != -1)
ptr = dictionary[ptr].prev;
dictionary[dictionary_ind].byte = dictionary[ptr].byte;
} else {
ptr = code;
while (dictionary[ptr].prev != -1)
ptr = dictionary[ptr].prev;
dictionary[dictionary_ind].byte = dictionary[ptr].byte;
}
dictionary[dictionary_ind].prev = prev;
dictionary[dictionary_ind].len = dictionary[prev].len + 1;
dictionary_ind++;
if ((dictionary_ind == (1 << (code_length + 1))) && (code_length < 11)) {
code_length++;
dictionary.resize(1 << (code_length + 1));
}
}
prev = code;
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
std::cout << "Invalid LZW code " << code << ", dictionary size " << static_cast<unsigned long>(dictionary.size()) << '\n';
throw std::runtime_error("LZW error: invalid code encountered");
}
int curCode = code;
match_len = dictionary[curCode].len;
while (curCode != -1) {
out[dictionary[curCode].len - 1] = dictionary[curCode].byte;
if (dictionary[curCode].prev == curCode) {
std::cout << "Internal error; self-reference detected." << '\n';
throw std::runtime_error("Internal error in decompress: self-reference");
}
curCode = dictionary[curCode].prev;
}
out += match_len;
}
}
std::vector<uint8_t> Gif::readSubBlocks(const uint8_t *&buffer) {
std::vector<uint8_t> data;
uint8_t block_size = *buffer;
buffer++;
while (block_size != 0) {
data.insert(data.end(), buffer, buffer + block_size);
buffer += block_size;
block_size = *buffer;
buffer++;
}
return data;
}
std::vector<uint8_t> Gif::processImageDescriptor(const uint8_t *&buffer, const std::vector<RGB> &gct, int resolution_bits) {
ImageDescriptor image_descriptor;
readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor));
uint8_t lzw_code_size;
readBytes(buffer, &lzw_code_size, sizeof(uint8_t));
std::vector<uint8_t> compressed_data = readSubBlocks(buffer);
int uncompressed_data_length = image_descriptor.image_width * image_descriptor.image_height;
std::vector<uint8_t> uncompressed_data(uncompressed_data_length);
decompress(lzw_code_size, compressed_data.data(), static_cast<int>(compressed_data.size()), uncompressed_data.data());
return uncompressed_data;
}
std::vector<uint32_t> Gif::loadPalette(const uint8_t *buffer) {
uint8_t header[6];
std::memcpy(header, buffer, 6);
buffer += 6;
ScreenDescriptor screen_descriptor;
std::memcpy(&screen_descriptor, buffer, sizeof(ScreenDescriptor));
buffer += sizeof(ScreenDescriptor);
std::vector<uint32_t> global_color_table;
if (screen_descriptor.fields & 0x80) {
int global_color_table_size = 1 << (((screen_descriptor.fields & 0x07) + 1));
global_color_table.resize(global_color_table_size);
for (int i = 0; i < global_color_table_size; ++i) {
uint8_t r = buffer[0];
uint8_t g = buffer[1];
uint8_t b = buffer[2];
global_color_table[i] = (r << 16) | (g << 8) | b;
buffer += 3;
}
}
return global_color_table;
}
std::vector<uint8_t> Gif::processGifStream(const uint8_t *buffer, uint16_t &w, uint16_t &h) {
uint8_t header[6];
std::memcpy(header, buffer, 6);
buffer += 6;
std::string headerStr(reinterpret_cast<char *>(header), 6);
if (headerStr != "GIF87a" && headerStr != "GIF89a") {
std::cout << "Formato de archivo GIF inválido: " << headerStr << '\n';
throw std::runtime_error("Formato de archivo GIF inválido.");
}
ScreenDescriptor screen_descriptor;
readBytes(buffer, &screen_descriptor, sizeof(ScreenDescriptor));
w = screen_descriptor.width;
h = screen_descriptor.height;
int color_resolution_bits = ((screen_descriptor.fields & 0x70) >> 4) + 1;
std::vector<RGB> global_color_table;
if (screen_descriptor.fields & 0x80) {
int global_color_table_size = 1 << (((screen_descriptor.fields & 0x07) + 1));
global_color_table.resize(global_color_table_size);
std::memcpy(global_color_table.data(), buffer, 3 * global_color_table_size);
buffer += 3 * global_color_table_size;
}
uint8_t block_type = *buffer++;
while (block_type != TRAILER) {
if (block_type == EXTENSION_INTRODUCER) {
uint8_t extension_label = *buffer++;
switch (extension_label) {
case GRAPHIC_CONTROL: {
uint8_t blockSize = *buffer++;
buffer += blockSize;
uint8_t subBlockSize = *buffer++;
while (subBlockSize != 0) {
buffer += subBlockSize;
subBlockSize = *buffer++;
}
break;
}
case APPLICATION_EXTENSION:
case COMMENT_EXTENSION:
case PLAINTEXT_EXTENSION: {
uint8_t blockSize = *buffer++;
buffer += blockSize;
uint8_t subBlockSize = *buffer++;
while (subBlockSize != 0) {
buffer += subBlockSize;
subBlockSize = *buffer++;
}
break;
}
default: {
uint8_t blockSize = *buffer++;
buffer += blockSize;
uint8_t subBlockSize = *buffer++;
while (subBlockSize != 0) {
buffer += subBlockSize;
subBlockSize = *buffer++;
}
break;
}
}
} else if (block_type == IMAGE_DESCRIPTOR) {
return processImageDescriptor(buffer, global_color_table, color_resolution_bits);
} else {
std::cout << "Unrecognized block type: 0x" << std::hex << static_cast<int>(block_type) << std::dec << '\n';
return std::vector<uint8_t>{};
}
block_type = *buffer++;
}
return std::vector<uint8_t>{};
}
std::vector<uint8_t> Gif::loadGif(const uint8_t *buffer, uint16_t &w, uint16_t &h) {
return processGifStream(buffer, w, h);
}
} // namespace GIF

View File

@@ -0,0 +1,92 @@
#pragma once
#include <cstdint> // Para uint8_t, uint16_t, uint32_t
#include <vector> // Para vector
namespace GIF {
// Constantes definidas con constexpr, en lugar de macros
constexpr uint8_t EXTENSION_INTRODUCER = 0x21;
constexpr uint8_t IMAGE_DESCRIPTOR = 0x2C;
constexpr uint8_t TRAILER = 0x3B;
constexpr uint8_t GRAPHIC_CONTROL = 0xF9;
constexpr uint8_t APPLICATION_EXTENSION = 0xFF;
constexpr uint8_t COMMENT_EXTENSION = 0xFE;
constexpr uint8_t PLAINTEXT_EXTENSION = 0x01;
#pragma pack(push, 1)
struct ScreenDescriptor {
uint16_t width;
uint16_t height;
uint8_t fields;
uint8_t background_color_index;
uint8_t pixel_aspect_ratio;
};
struct RGB {
uint8_t r, g, b;
};
struct ImageDescriptor {
uint16_t image_left_position;
uint16_t image_top_position;
uint16_t image_width;
uint16_t image_height;
uint8_t fields;
};
#pragma pack(pop)
struct DictionaryEntry {
uint8_t byte;
int prev;
int len;
};
struct Extension {
uint8_t extension_code;
uint8_t block_size;
};
struct GraphicControlExtension {
uint8_t fields;
uint16_t delay_time;
uint8_t transparent_color_index;
};
struct ApplicationExtension {
uint8_t application_id[8];
uint8_t version[3];
};
struct PlaintextExtension {
uint16_t left, top, width, height;
uint8_t cell_width, cell_height;
uint8_t foreground_color, background_color;
};
class Gif {
public:
// Descompone (uncompress) el bloque comprimido usando LZW.
// Este método puede lanzar std::runtime_error en caso de error.
void decompress(int code_length, const uint8_t *input, int input_length, uint8_t *out);
// Carga la paleta (global color table) a partir de un buffer,
// retornándola en un vector de uint32_t (cada color se compone de R, G, B).
std::vector<uint32_t> loadPalette(const uint8_t *buffer);
// Carga el stream GIF; devuelve un vector con los datos de imagen sin comprimir y
// asigna el ancho y alto mediante referencias.
std::vector<uint8_t> loadGif(const uint8_t *buffer, uint16_t &w, uint16_t &h);
private:
// Lee los sub-bloques de datos y los acumula en un std::vector<uint8_t>.
std::vector<uint8_t> readSubBlocks(const uint8_t *&buffer);
// Procesa el Image Descriptor y retorna el vector de datos sin comprimir.
std::vector<uint8_t> processImageDescriptor(const uint8_t *&buffer, const std::vector<RGB> &gct, int resolution_bits);
// Procesa el stream completo del GIF y devuelve los datos sin comprimir.
std::vector<uint8_t> processGifStream(const uint8_t *buffer, uint16_t &w, uint16_t &h);
};
} // namespace GIF

View File

@@ -0,0 +1,624 @@
#include "screen.hpp"
#include <SDL3/SDL.h> // Para SDL_SetRenderTarget, SDL_RenderTexture, SDL_SetRenderDrawColor, SDL_SetRenderVSync, SDL_LogCategory, SDL_GetError, SDL_LogError, SDL_LogInfo, SDL_RendererLogicalPresentation, SDL_SetRenderLogicalPresentation, SDL_CreateTexture, SDL_DestroyTexture, SDL_DestroyWindow, SDL_GetDisplayName, SDL_GetTicks, SDL_Quit, SDL_RENDERER_VSYNC_DISABLED, SDL_RenderClear, SDL_CreateRenderer, SDL_CreateWindow, SDL_DestroyRenderer, SDL_DisplayID, SDL_FRect, SDL_GetCurrentDisplayMode, SDL_GetDisplays, SDL_GetRenderTarget, SDL_GetWindowPosition, SDL_GetWindowSize, SDL_Init, SDL_LogWarn, SDL_PixelFormat, SDL_RenderFillRect, SDL_RenderPresent, SDL_SetHint, SDL_SetRenderDrawBlendMode, SDL_SetTextureScaleMode, SDL_SetWindowFullscreen, SDL_SetWindowPosition, SDL_SetWindowSize, SDL_TextureAccess, SDL_free, SDL_BLENDMODE_BLEND, SDL_HINT_RENDER_DRIVER, SDL_INIT_VIDEO, SDL_ScaleMode, SDL_WINDOW_FULLSCREEN, SDL_WindowFlags
#include <algorithm> // Para min, max
#include <cstring> // Para memcpy
#include <iostream> // Para std::cout
#include <memory> // Para allocator, shared_ptr, unique_ptr, __shared_ptr_access, make_shared, make_unique
#include <string> // Para basic_string, operator+, char_traits, to_string, string
#include <vector> // Para vector
#include "asset.hpp" // Para Asset
#include "director.hpp" // Para Director::debug_config
#include "mouse.hpp" // Para updateCursorVisibility
#include "options.hpp" // Para Video, video, Window, window
#include "param.hpp" // Para Param, param, ParamGame, ParamDebug
#include "rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
#include "resource.hpp" // Para Resource
#include "text.hpp" // Para Text
#include "texture.hpp" // Para Texture
#include "ui/notifier.hpp" // Para Notifier
#include "ui/service_menu.hpp" // Para ServiceMenu
#include "utils.hpp" // Para toLower
// Singleton
Screen* Screen::instance = nullptr;
// Inicializa la instancia única del singleton
void Screen::init() {
Screen::instance = new Screen();
Screen::initShaders(); // Llamar aquí para que Screen::get() ya devuelva la instancia
}
// Libera la instancia
void Screen::destroy() { delete Screen::instance; }
// Obtiene la instancia
auto Screen::get() -> Screen* { return Screen::instance; }
// Constructor
Screen::Screen()
: window_(nullptr),
renderer_(nullptr),
game_canvas_(nullptr),
service_menu_(nullptr),
notifier_(nullptr),
src_rect_(SDL_FRect{.x = 0, .y = 0, .w = param.game.width, .h = param.game.height}),
dst_rect_(SDL_FRect{.x = 0, .y = 0, .w = param.game.width, .h = param.game.height}) {
// Arranca SDL VIDEO, crea la ventana y el renderizador
initSDLVideo();
// Crea la textura de destino
game_canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);
SDL_SetTextureScaleMode(game_canvas_, SDL_SCALEMODE_NEAREST);
// Inicializar buffer de píxeles para SDL3GPU
pixel_buffer_.resize(static_cast<size_t>(param.game.width) * static_cast<size_t>(param.game.height));
// Crea el objeto de texto
createText();
#ifdef _DEBUG
debug_info_.text = text_;
setDebugInfoEnabled(Director::debug_config.show_render_info);
#endif
// Renderizar una vez la textura vacía para que tenga contenido válido antes de inicializar los shaders (evita pantalla negra)
SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);
// Limpiar renderer
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
SDL_RenderClear(renderer_);
SDL_RenderPresent(renderer_);
}
// Destructor
Screen::~Screen() {
SDL_DestroyTexture(game_canvas_);
SDL_DestroyRenderer(renderer_);
SDL_DestroyWindow(window_);
}
// Limpia la pantalla
void Screen::clean(Color color) {
SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 0xFF);
SDL_RenderClear(renderer_);
}
// Prepara para empezar a dibujar en la textura de juego
void Screen::start() { SDL_SetRenderTarget(renderer_, game_canvas_); }
// Vuelca el contenido del renderizador en pantalla
void Screen::render() {
fps_.increment();
renderOverlays(); // Renderiza todos los overlays y efectos
renderPresent(); // Renderiza el contenido del game_canvas_
}
// Vuelca el contenido del renderizador en pantalla exceptuando ciertas partes
void Screen::coreRender() {
/*fps_.increment();
#ifdef _DEBUG
renderInfo();
#endif*/
renderPresent(); // Renderiza el contenido del game_canvas_
}
// Renderiza el contenido del game_canvas_
void Screen::renderPresent() {
#ifndef NO_SHADERS
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
// Leer píxeles de game_canvas_ con la API SDL3 (devuelve SDL_Surface*)
SDL_SetRenderTarget(renderer_, game_canvas_);
SDL_Surface* surface = SDL_RenderReadPixels(renderer_, nullptr);
if (surface != nullptr) {
if (surface->format == SDL_PIXELFORMAT_ARGB8888) {
std::memcpy(pixel_buffer_.data(), surface->pixels, pixel_buffer_.size() * sizeof(Uint32));
} else {
SDL_Surface* converted = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ARGB8888);
if (converted != nullptr) {
std::memcpy(pixel_buffer_.data(), converted->pixels, pixel_buffer_.size() * sizeof(Uint32));
SDL_DestroySurface(converted);
}
}
SDL_DestroySurface(surface);
}
SDL_SetRenderTarget(renderer_, nullptr);
// Subir a GPU y presentar con PostFX
shader_backend_->uploadPixels(pixel_buffer_.data(), param.game.width, param.game.height);
shader_backend_->render();
return;
}
#endif
// Fallback: SDL_Renderer
SDL_SetRenderTarget(renderer_, nullptr);
clean();
SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);
SDL_RenderPresent(renderer_);
}
// Establece el modo de video
void Screen::setFullscreenMode() {
SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
}
// Camibia entre pantalla completa y ventana
void Screen::toggleFullscreen() {
Options::video.fullscreen = !Options::video.fullscreen;
setFullscreenMode();
}
// Cambia el tamaño de la ventana
void Screen::setWindowZoom(int zoom) {
Options::window.zoom = zoom;
adjustWindowSize();
}
// Reduce el tamaño de la ventana
auto Screen::decWindowSize() -> bool {
if (!Options::video.fullscreen) {
const int PREVIOUS_ZOOM = Options::window.zoom;
--Options::window.zoom;
Options::window.zoom = std::max(Options::window.zoom, 1);
if (Options::window.zoom != PREVIOUS_ZOOM) {
adjustWindowSize();
return true;
}
}
return false;
}
// Aumenta el tamaño de la ventana
auto Screen::incWindowSize() -> bool {
if (!Options::video.fullscreen) {
const int PREVIOUS_ZOOM = Options::window.zoom;
++Options::window.zoom;
Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);
if (Options::window.zoom != PREVIOUS_ZOOM) {
adjustWindowSize();
return true;
}
}
return false;
}
// Recibe deltaTime de las secciones y actualiza la lógica
void Screen::update(float delta_time) {
fps_.calculate(SDL_GetTicks());
shake_effect_.update(src_rect_, dst_rect_, delta_time);
flash_effect_.update(delta_time);
if (service_menu_ != nullptr) {
service_menu_->update(delta_time);
}
if (notifier_ != nullptr) {
notifier_->update(delta_time);
}
Mouse::updateCursorVisibility();
}
// Actualiza los elementos mínimos
void Screen::coreUpdate() {
fps_.calculate(SDL_GetTicks());
Mouse::updateCursorVisibility();
}
// Actualiza y dibuja el efecto de flash en la pantalla
void Screen::renderFlash() {
if (flash_effect_.isRendarable()) {
SDL_SetRenderDrawColor(renderer_, flash_effect_.color.r, flash_effect_.color.g, flash_effect_.color.b, 0xFF);
SDL_RenderClear(renderer_);
}
}
// Aplica el efecto de agitar la pantalla
void Screen::renderShake() {
if (shake_effect_.enabled) {
// Guarda el renderizador actual para dejarlo despues como estaba
auto* current_target = SDL_GetRenderTarget(renderer_);
// Crea una textura temporal
auto* temp_texture = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);
// Vuelca game_canvas_ a la textura temporal
SDL_SetRenderTarget(renderer_, temp_texture);
SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);
// Vuelca textura temporal a game_canvas_
SDL_SetRenderTarget(renderer_, game_canvas_);
SDL_RenderTexture(renderer_, temp_texture, &src_rect_, &dst_rect_);
// Elimina la textura temporal
SDL_DestroyTexture(temp_texture);
// Restaura el renderizador de destino original
SDL_SetRenderTarget(renderer_, current_target);
}
}
#ifdef _DEBUG
// Muestra información por pantalla
void Screen::renderInfo() const {
if (debug_info_.show) {
const Color GOLD(0xFF, 0xD7, 0x00);
const Color GOLD_SHADOW = GOLD.DARKEN(150);
// Construir texto: fps - driver - preset
std::string info_text = std::to_string(fps_.last_value) + " fps";
// Driver GPU
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
const std::string DRIVER = shader_backend_->getDriverName();
if (!DRIVER.empty()) {
info_text += " - " + toLower(DRIVER);
}
} else {
info_text += " - sdl";
}
// Shader + preset
if (Options::video.shader.enabled) {
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
const std::string PRESET_NAME = Options::crtpi_presets.empty() ? "" : Options::crtpi_presets.at(static_cast<size_t>(Options::video.shader.current_crtpi_preset)).name;
info_text += " - crtpi " + toLower(PRESET_NAME);
} else {
const std::string PRESET_NAME = Options::postfx_presets.empty() ? "" : Options::postfx_presets.at(static_cast<size_t>(Options::video.shader.current_postfx_preset)).name;
info_text += " - postfx " + toLower(PRESET_NAME);
if (Options::video.supersampling.enabled) { info_text += " (ss)"; }
}
}
// Centrado arriba
const int TEXT_WIDTH = debug_info_.text->length(info_text);
const int X_POS = (static_cast<int>(param.game.width) - TEXT_WIDTH) / 2;
debug_info_.text->writeDX(Text::COLOR | Text::STROKE, X_POS, 1, info_text, 1, GOLD, 1, GOLD_SHADOW);
#ifdef RECORDING
const std::string REC_TEXT = "recording";
const int REC_WIDTH = debug_info_.text->length(REC_TEXT);
const int REC_X = (static_cast<int>(param.game.width) - REC_WIDTH) / 2;
debug_info_.text->writeDX(Text::COLOR | Text::STROKE, REC_X, 1 + debug_info_.text->getCharacterSize(), REC_TEXT, 1, GOLD, 1, GOLD_SHADOW);
#endif
}
}
#endif
// Inicializa shaders (SDL3GPU)
void Screen::initShaders() {
#ifndef NO_SHADERS
auto* self = Screen::get();
if (self == nullptr) {
std::cout << "Screen::initShaders: instance is null, skipping" << '\n';
return;
}
if (!self->shader_backend_) {
self->shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
const std::string FALLBACK_DRIVER = "none";
self->shader_backend_->setPreferredDriver(
Options::video.gpu.acceleration ? Options::video.gpu.preferred_driver : FALLBACK_DRIVER);
}
if (!self->shader_backend_->isHardwareAccelerated()) {
const bool ok = self->shader_backend_->init(self->window_, self->game_canvas_, "", "");
std::cout << "Screen::initShaders: SDL3GPUShader::init() = " << (ok ? "OK" : "FAILED") << '\n';
}
if (self->shader_backend_ && self->shader_backend_->isHardwareAccelerated()) {
self->shader_backend_->setLinearUpscale(Options::video.supersampling.linear_upscale);
self->shader_backend_->setDownscaleAlgo(Options::video.supersampling.downscale_algo);
self->shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
if (!Options::video.shader.enabled) {
// Passthrough: POSTFX con parámetros a cero
self->shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
self->shader_backend_->setPostFXParams(Rendering::PostFXParams{});
} else if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
self->shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
self->applyCurrentCrtPiPreset();
} else {
self->shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
self->applyCurrentPostFXPreset();
}
}
#endif
}
// Calcula el tamaño de la ventana
void Screen::adjustWindowSize() {
if (!Options::video.fullscreen) {
// Establece el nuevo tamaño
const int WIDTH = param.game.width * Options::window.zoom;
const int HEIGHT = param.game.height * Options::window.zoom;
int old_width;
int old_height;
SDL_GetWindowSize(window_, &old_width, &old_height);
int old_pos_x;
int old_pos_y;
SDL_GetWindowPosition(window_, &old_pos_x, &old_pos_y);
const int NEW_POS_X = old_pos_x + ((old_width - WIDTH) / 2);
const int NEW_POS_Y = old_pos_y + ((old_height - HEIGHT) / 2);
SDL_SetWindowPosition(window_, std::max(NEW_POS_X, WINDOWS_DECORATIONS), std::max(NEW_POS_Y, 0));
SDL_SetWindowSize(window_, WIDTH, HEIGHT);
}
}
// Renderiza todos los overlays y efectos
void Screen::renderOverlays() {
// Dibuja efectos y elementos sobre el game_canvas_
renderShake();
renderFlash();
renderAttenuate();
service_menu_->render();
notifier_->render();
#ifdef _DEBUG
renderInfo();
#endif
}
// Atenua la pantalla
void Screen::renderAttenuate() {
if (attenuate_effect_) {
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 64);
SDL_RenderFillRect(renderer_, nullptr);
}
}
// Arranca SDL VIDEO y crea la ventana
auto Screen::initSDLVideo() -> bool {
// Inicializar SDL
if (!SDL_Init(SDL_INIT_VIDEO)) {
std::cout << "FATAL: Failed to initialize SDL_VIDEO! SDL Error: " << SDL_GetError() << '\n';
return false;
}
// Obtener información de la pantalla
getDisplayInfo();
// Configurar hint para renderizado
#ifdef __APPLE__
if (!SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal")) {
std::cout << "Warning: Failed to set Metal hint!" << '\n';
}
#endif
// Crear ventana
#ifdef __APPLE__
SDL_WindowFlags window_flags = SDL_WINDOW_METAL;
#else
SDL_WindowFlags window_flags = 0;
#endif
if (Options::video.fullscreen) {
window_flags |= SDL_WINDOW_FULLSCREEN;
}
window_flags |= SDL_WINDOW_HIDDEN;
window_ = SDL_CreateWindow(
Options::window.caption.c_str(),
param.game.width * Options::window.zoom,
param.game.height * Options::window.zoom,
window_flags);
if (window_ == nullptr) {
std::cout << "FATAL: Failed to create window! SDL Error: " << SDL_GetError() << '\n';
SDL_Quit();
return false;
}
// Crear renderer
renderer_ = SDL_CreateRenderer(window_, nullptr);
if (renderer_ == nullptr) {
std::cout << "FATAL: Failed to create renderer! SDL Error: " << SDL_GetError() << '\n';
SDL_DestroyWindow(window_);
window_ = nullptr;
SDL_Quit();
return false;
}
// Configurar renderer
SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
return true;
}
// Obtiene información sobre la pantalla
void Screen::getDisplayInfo() {
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays != nullptr) {
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
// Guarda información del monitor en display_monitor_
const char* first_display_name = SDL_GetDisplayName(displays[0]);
display_monitor_.name = (first_display_name != nullptr) ? first_display_name : "Unknown";
display_monitor_.width = static_cast<int>(dm->w);
display_monitor_.height = static_cast<int>(dm->h);
display_monitor_.refresh_rate = static_cast<int>(dm->refresh_rate);
// Calcula el máximo factor de zoom que se puede aplicar a la pantalla
Options::window.max_zoom = std::min(dm->w / param.game.width, dm->h / param.game.height);
Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);
// Obtiene la cadena con la información sobre la resolución y el refresco
Options::video.info = std::to_string(dm->w) + "x" +
std::to_string(dm->h) + " @ " +
std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz";
// Calcula el máximo factor de zoom que se puede aplicar a la pantalla
const int MAX_ZOOM = std::min(dm->w / param.game.width, (dm->h - WINDOWS_DECORATIONS) / param.game.height);
// Normaliza los valores de zoom
Options::window.zoom = std::min(Options::window.zoom, MAX_ZOOM);
SDL_free(displays);
}
}
// Alterna activar/desactivar shaders
void Screen::toggleShaders() {
Options::video.shader.enabled = !Options::video.shader.enabled;
auto* self = Screen::get();
if (self != nullptr) {
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
self->applyCurrentCrtPiPreset();
} else {
self->applyCurrentPostFXPreset();
}
}
}
// Cambia entre PostFX y CrtPi
void Screen::nextShader() {
auto* self = Screen::get();
if (self == nullptr || !self->shader_backend_ || !self->shader_backend_->isHardwareAccelerated()) { return; }
const auto NEXT = (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX)
? Rendering::ShaderType::CRTPI
: Rendering::ShaderType::POSTFX;
Options::video.shader.current_shader = NEXT;
self->shader_backend_->setActiveShader(NEXT);
if (NEXT == Rendering::ShaderType::CRTPI) {
self->applyCurrentCrtPiPreset();
} else {
self->applyCurrentPostFXPreset();
}
}
// Avanza al siguiente preset PostFX
void Screen::nextPostFXPreset() {
if (Options::postfx_presets.empty()) { return; }
Options::video.shader.current_postfx_preset = (Options::video.shader.current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
auto* self = Screen::get();
if (self != nullptr) {
self->applyCurrentPostFXPreset();
}
}
// Avanza al siguiente preset CrtPi
void Screen::nextCrtPiPreset() {
if (Options::crtpi_presets.empty()) { return; }
Options::video.shader.current_crtpi_preset = (Options::video.shader.current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
auto* self = Screen::get();
if (self != nullptr) {
self->applyCurrentCrtPiPreset();
}
}
// Alterna supersampling
void Screen::toggleSupersampling() {
Options::video.supersampling.enabled = !Options::video.supersampling.enabled;
auto* self = Screen::get();
if (self != nullptr && self->shader_backend_ && self->shader_backend_->isHardwareAccelerated()) {
self->shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
self->applyCurrentCrtPiPreset();
} else {
self->applyCurrentPostFXPreset();
}
}
}
// Aplica el preset PostFX activo al backend
void Screen::applyCurrentPostFXPreset() {
if (!shader_backend_) { return; }
shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
Rendering::PostFXParams p{};
if (Options::video.shader.enabled && !Options::postfx_presets.empty()) {
const auto& preset = Options::postfx_presets.at(static_cast<size_t>(Options::video.shader.current_postfx_preset));
p.vignette = preset.vignette;
p.scanlines = preset.scanlines;
p.chroma = preset.chroma;
p.mask = preset.mask;
p.gamma = preset.gamma;
p.curvature = preset.curvature;
p.bleeding = preset.bleeding;
p.flicker = preset.flicker;
std::cout << "Screen::applyCurrentPostFXPreset: preset='" << preset.name << "' scan=" << p.scanlines << " vign=" << p.vignette << " chroma=" << p.chroma << '\n';
}
shader_backend_->setPostFXParams(p);
}
// Aplica el preset CrtPi activo al backend
void Screen::applyCurrentCrtPiPreset() {
if (!shader_backend_) { return; }
if (Options::video.shader.enabled && !Options::crtpi_presets.empty()) {
const auto& preset = Options::crtpi_presets.at(static_cast<size_t>(Options::video.shader.current_crtpi_preset));
Rendering::CrtPiParams p{
.scanline_weight = preset.scanline_weight,
.scanline_gap_brightness = preset.scanline_gap_brightness,
.bloom_factor = preset.bloom_factor,
.input_gamma = preset.input_gamma,
.output_gamma = preset.output_gamma,
.mask_brightness = preset.mask_brightness,
.curvature_x = preset.curvature_x,
.curvature_y = preset.curvature_y,
.mask_type = preset.mask_type,
.enable_scanlines = preset.enable_scanlines,
.enable_multisample = preset.enable_multisample,
.enable_gamma = preset.enable_gamma,
.enable_curvature = preset.enable_curvature,
.enable_sharper = preset.enable_sharper,
};
shader_backend_->setCrtPiParams(p);
std::cout << "Screen::applyCurrentCrtPiPreset: preset='" << preset.name << "'" << '\n';
}
}
// Alterna entre activar y desactivar el escalado entero
void Screen::toggleIntegerScale() {
Options::video.integer_scale = !Options::video.integer_scale;
SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
if (shader_backend_) {
shader_backend_->setScaleMode(Options::video.integer_scale);
}
}
// Alterna entre activar y desactivar el V-Sync
void Screen::toggleVSync() {
Options::video.vsync = !Options::video.vsync;
SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
if (shader_backend_) {
shader_backend_->setVSync(Options::video.vsync);
}
}
// Establece el estado del V-Sync
void Screen::setVSync(bool enabled) {
Options::video.vsync = enabled;
SDL_SetRenderVSync(renderer_, enabled ? 1 : SDL_RENDERER_VSYNC_DISABLED);
}
// Obtiene los punteros a los singletones
void Screen::getSingletons() {
service_menu_ = ServiceMenu::get();
notifier_ = Notifier::get();
#ifdef _DEBUG
// Actualizar la fuente de debug a 8bithud (ahora Resource está disponible)
if (Resource::get() != nullptr) {
auto hud_text = Resource::get()->getText("8bithud");
if (hud_text) {
debug_info_.text = hud_text;
}
}
#endif
}
// Aplica los valores de las opciones
void Screen::applySettings() {
SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
SDL_SetRenderLogicalPresentation(Screen::get()->getRenderer(), param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
setFullscreenMode();
adjustWindowSize();
}
// Crea el objeto de texto
void Screen::createText() {
auto texture = std::make_shared<Texture>(getRenderer(), Asset::get()->getPath("aseprite.png"));
text_ = std::make_shared<Text>(texture, Asset::get()->getPath("aseprite.txt"));
}

View File

@@ -0,0 +1,254 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect, SDL_HideWindow, SDL_Renderer, SDL_ShowWindow, Uint32, SDL_Texture, SDL_Window
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "color.hpp" // Para Color
#include "options.hpp" // Para VideoOptions, video
#include "rendering/shader_backend.hpp" // Para Rendering::ShaderType
// Forward declarations
class Notifier;
class ServiceMenu;
class Text;
// --- Clase Screen: gestiona la ventana, el renderizador y los efectos visuales globales (singleton) ---
class Screen {
public:
// --- Métodos de singleton ---
static void init(); // Inicializa el objeto Screen
static void destroy(); // Libera el objeto Screen
static auto get() -> Screen*; // Obtiene el puntero al objeto Screen
// --- Métodos principales ---
void update(float delta_time); // Recibe deltaTime de las secciones y actualiza la lógica
void coreUpdate(); // Actualiza los elementos mínimos
void clean(Color color = Color(0x00, 0x00, 0x00)); // Limpia la pantalla
void start(); // Prepara para empezar a dibujar en la textura de juego
void render(); // Vuelca el contenido del renderizador en pantalla
void coreRender(); // Vuelca el contenido del renderizador en pantalla exceptuando ciertas partes
// --- Configuración de ventana y render ---
void setFullscreenMode(); // Establece el modo de pantalla completa
void toggleFullscreen(); // Cambia entre pantalla completa y ventana
void setWindowZoom(int zoom); // Cambia el tamaño de la ventana
auto decWindowSize() -> bool; // Reduce el tamaño de la ventana
auto incWindowSize() -> bool; // Aumenta el tamaño de la ventana
void applySettings(); // Aplica los valores de las opciones
static void initShaders(); // Inicializa shaders (SDL3GPU)
// --- Efectos visuales ---
void shake(int desp = 2, float delay_s = 0.05F, float duration_s = 0.133F) { shake_effect_.enable(src_rect_, dst_rect_, desp, delay_s, duration_s); }
void flash(Color color, float duration_s = 0.167F, float delay_s = 0.0F) { flash_effect_ = FlashEffect(true, duration_s, delay_s, color); }
static void toggleShaders(); // Alterna activar/desactivar shaders
static void nextShader(); // Cambia entre PostFX y CrtPi
static void nextPostFXPreset(); // Avanza al siguiente preset PostFX
static void nextCrtPiPreset(); // Avanza al siguiente preset CrtPi
static void toggleSupersampling(); // Alterna supersampling
void toggleIntegerScale();
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
void setVSync(bool enabled); // Establece el estado del V-Sync
void attenuate(bool value) { attenuate_effect_ = value; } // Atenúa la pantalla
// --- Getters ---
auto getRenderer() -> SDL_Renderer* { return renderer_; } // Obtiene el renderizador
void show() { SDL_ShowWindow(window_); } // Muestra la ventana
void hide() { SDL_HideWindow(window_); } // Oculta la ventana
void getSingletons(); // Obtiene los punteros a los singletones
[[nodiscard]] static auto getVSync() -> bool { return Options::video.vsync; } // Obtiene el valor de V-Sync
[[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; } // Obtiene el puntero al texto de Screen
// --- Display Monitor getters ---
[[nodiscard]] auto getDisplayMonitorName() const -> std::string { return display_monitor_.name; }
[[nodiscard]] auto getDisplayMonitorWidth() const -> int { return display_monitor_.width; }
[[nodiscard]] auto getDisplayMonitorHeight() const -> int { return display_monitor_.height; }
[[nodiscard]] auto getDisplayMonitorRefreshRate() const -> int { return display_monitor_.refresh_rate; }
#ifdef _DEBUG
// --- Debug ---
void toggleDebugInfo() { debug_info_.show = !debug_info_.show; }
void setDebugInfoEnabled(bool value) { debug_info_.show = value; }
#endif
private:
// --- Constantes ---
static constexpr int WINDOWS_DECORATIONS = 35; // Decoraciones de la ventana
// --- Estructuras privadas ---
struct DisplayMonitor {
std::string name;
int width;
int height;
int refresh_rate;
};
struct FPS {
Uint32 ticks{0}; // Tiempo en milisegundos desde que se comenzó a contar.
int frame_count{0}; // Número acumulado de frames en el intervalo.
int last_value{0}; // Número de frames calculado en el último segundo.
FPS() = default;
void increment() { frame_count++; }
auto calculate(Uint32 current_ticks) -> int {
if (current_ticks - ticks >= 1000) {
last_value = frame_count;
frame_count = 0;
ticks = current_ticks;
}
return last_value;
}
};
// Efecto de flash en pantalla: pinta la pantalla de un color durante un tiempo
struct FlashEffect {
bool enabled; // Indica si el efecto está activo
float duration_s; // Duración total del efecto en segundos
float delay_s; // Retraso antes de mostrar el flash en segundos
float timer_s; // Timer en segundos (contador decreciente)
Color color; // Color del flash
explicit FlashEffect(bool enabled = false, float duration_s = 0.0F, float delay_s = 0.0F, Color color = Color(0xFF, 0xFF, 0xFF))
: enabled(enabled),
duration_s(duration_s),
delay_s(delay_s),
timer_s(duration_s),
color(color) {}
void update(float delta_time) {
if (enabled && timer_s > 0.0F) {
timer_s -= delta_time;
if (timer_s <= 0.0F) {
enabled = false;
}
}
}
[[nodiscard]] auto isRendarable() const -> bool { return enabled && timer_s < duration_s - delay_s; }
};
// Efecto de sacudida/agitación de pantalla: mueve la imagen para simular un temblor
struct ShakeEffect {
int desp; // Desplazamiento máximo de la sacudida (en píxeles)
float delay_s; // Segundos entre cada movimiento de sacudida
float counter_s; // Timer para el siguiente movimiento (decreciente)
float duration_s; // Duración total del efecto en segundos
float remaining_s; // Tiempo restante de sacudida
int original_pos; // Posición original de la imagen (x)
int original_width; // Ancho original de la imagen
bool enabled; // Indica si el efecto está activo
explicit ShakeEffect(bool en = false, int dp = 2, float dl_s = 0.05F, float cnt_s = 0.0F, float len_s = 0.133F, float rem_s = 0.0F, int orig_pos = 0, int orig_width = 800)
: desp(dp),
delay_s(dl_s),
counter_s(cnt_s),
duration_s(len_s),
remaining_s(rem_s),
original_pos(orig_pos),
original_width(orig_width),
enabled(en) {}
// Activa el efecto de sacudida y guarda la posición y tamaño originales
void enable(SDL_FRect& src_rect, SDL_FRect& dst_rect, int new_desp = -1, float new_delay_s = -1.0F, float new_duration_s = -1.0F) {
if (!enabled) {
enabled = true;
original_pos = src_rect.x;
original_width = src_rect.w;
// Usar nuevos valores si se proporcionan, sino mantener los actuales
if (new_desp != -1) {
desp = new_desp;
}
if (new_delay_s >= 0.0F) {
delay_s = new_delay_s;
}
if (new_duration_s >= 0.0F) {
duration_s = new_duration_s;
}
src_rect.w -= desp;
dst_rect.w = src_rect.w;
}
remaining_s = duration_s;
counter_s = delay_s;
}
// Actualiza el estado del efecto de sacudida
void update(SDL_FRect& src_rect, SDL_FRect& dst_rect, float delta_time) {
if (enabled) {
counter_s -= delta_time;
if (counter_s <= 0.0F) {
counter_s = delay_s;
// Alternar desplazamiento basado en tiempo restante
const bool SHAKE_LEFT = static_cast<int>(remaining_s * 30.0F) % 2 == 0; // ~30 cambios por segundo
const auto SRC_DESP = SHAKE_LEFT ? 0 : desp;
const auto DST_DESP = SHAKE_LEFT ? desp : 0;
src_rect.x = original_pos + SRC_DESP;
dst_rect.x = original_pos + DST_DESP;
remaining_s -= delay_s;
if (remaining_s <= 0.0F) {
enabled = false;
src_rect.x = original_pos;
src_rect.w = original_width;
dst_rect.x = original_pos;
dst_rect.w = original_width;
}
}
}
}
[[nodiscard]] auto isEnabled() const -> bool { return enabled; }
};
#ifdef _DEBUG
struct Debug {
std::shared_ptr<Text> text;
bool show = false;
};
#endif
// --- Objetos y punteros ---
SDL_Window* window_; // Ventana de la aplicación
SDL_Renderer* renderer_; // El renderizador de la ventana
SDL_Texture* game_canvas_; // Textura donde se dibuja todo antes de volcarse al renderizador
ServiceMenu* service_menu_; // Objeto para mostrar el menú de servicio
Notifier* notifier_; // Objeto para mostrar las notificaciones por pantalla
std::shared_ptr<Text> text_; // Objeto para escribir texto en pantalla
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend de shaders (SDL3GPU)
// --- Variables de estado ---
SDL_FRect src_rect_; // Coordenadas de origen para dibujar la textura del juego
SDL_FRect dst_rect_; // Coordenadas destino para dibujar la textura del juego
std::vector<Uint32> pixel_buffer_; // Buffer de píxeles para SDL_RenderReadPixels
FPS fps_; // Gestión de frames por segundo
FlashEffect flash_effect_; // Efecto de flash en pantalla
ShakeEffect shake_effect_; // Efecto de agitar la pantalla
bool attenuate_effect_ = false; // Indica si la pantalla ha de estar atenuada
DisplayMonitor display_monitor_; // Información del monitor actual
#ifdef _DEBUG
Debug debug_info_; // Información de debug
#endif
// --- Métodos internos ---
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
void renderFlash(); // Dibuja el efecto de flash en la pantalla
void renderShake(); // Aplica el efecto de agitar la pantalla
void renderInfo() const; // Muestra información por pantalla
void renderPresent(); // Selecciona y ejecuta el método de renderizado adecuado
void applyCurrentPostFXPreset(); // Aplica el preset PostFX activo al backend
void applyCurrentCrtPiPreset(); // Aplica el preset CrtPi activo al backend
void adjustWindowSize(); // Calcula el tamaño de la ventana
void getDisplayInfo(); // Obtiene información sobre la pantalla
void renderOverlays(); // Renderiza todos los overlays y efectos
void renderAttenuate(); // Atenúa la pantalla
void createText(); // Crea el objeto de texto
// --- Constructores y destructor privados (singleton) ---
Screen(); // Constructor privado
~Screen(); // Destructor privado
// --- Instancia singleton ---
static Screen* instance; // Instancia única de Screen
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,154 @@
#pragma once
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include <string>
#include <utility>
#include "rendering/shader_backend.hpp"
// PostFX uniforms pushed to fragment stage each frame.
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.
struct PostFXUniforms {
float vignette_strength;
float chroma_strength;
float scanline_strength;
float screen_height;
float mask_strength;
float gamma_strength;
float curvature;
float bleeding;
float pixel_scale;
float time;
float oversample;
float flicker;
};
// CrtPi uniforms pushed to fragment stage each frame.
// 16 fields = 64 bytes — 4 × 16-byte alignment.
struct CrtPiUniforms {
float scanline_weight;
float scanline_gap_brightness;
float bloom_factor;
float input_gamma;
float output_gamma;
float mask_brightness;
float curvature_x;
float curvature_y;
int mask_type;
int enable_scanlines;
int enable_multisample;
int enable_gamma;
int enable_curvature;
int enable_sharper;
float texture_width;
float texture_height;
};
// Downscale uniforms for Lanczos downscale fragment stage.
// 1 int + 3 floats = 16 bytes.
struct DownscaleUniforms {
int algorithm;
float pad0;
float pad1;
float pad2;
};
namespace Rendering {
/**
* @brief Backend de shaders usando SDL3 GPU API (Metal en macOS, Vulkan/SPIR-V en Win/Linux)
*
* Pipeline: Surface pixels (CPU) → SDL_GPUTransferBuffer → SDL_GPUTexture (scene)
* → [Upscale →] PostFX/CrtPi render pass → [Lanczos downscale →] swapchain → present
*/
class SDL3GPUShader : public ShaderBackend {
public:
SDL3GPUShader() = default;
~SDL3GPUShader() override;
auto init(SDL_Window* window,
SDL_Texture* texture,
const std::string& vertex_source,
const std::string& fragment_source) -> bool override;
void render() override;
void setTextureSize(float width, float height) override {}
void cleanup() final;
void destroy();
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
[[nodiscard]] auto getDriverName() const -> std::string override { return driver_name_; }
void setPreferredDriver(const std::string& driver) override { preferred_driver_ = driver; }
void uploadPixels(const Uint32* pixels, int width, int height) override;
void setPostFXParams(const PostFXParams& p) override;
void setVSync(bool vsync) override;
void setScaleMode(bool integer_scale) override;
void setOversample(int factor) override;
void setLinearUpscale(bool linear) override;
[[nodiscard]] auto isLinearUpscale() const -> bool override { return linear_upscale_; }
void setDownscaleAlgo(int algo) override;
[[nodiscard]] auto getDownscaleAlgo() const -> int override { return downscale_algo_; }
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int> override;
void setActiveShader(ShaderType type) override;
void setCrtPiParams(const CrtPiParams& p) override;
[[nodiscard]] auto getActiveShader() const -> ShaderType override { return active_shader_; }
private:
static auto createShaderMSL(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
static auto createShaderSPIRV(SDL_GPUDevice* device,
const uint8_t* spv_code,
size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
auto createPipeline() -> bool;
auto createCrtPiPipeline() -> bool;
auto reinitTexturesAndBuffer() -> bool;
auto recreateScaledTexture(int factor) -> bool;
static auto calcSsFactor(float zoom) -> int;
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
SDL_Window* window_ = nullptr;
SDL_GPUDevice* device_ = nullptr;
SDL_GPUGraphicsPipeline* pipeline_ = nullptr;
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr;
SDL_GPUGraphicsPipeline* postfx_offscreen_pipeline_ = nullptr;
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr;
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr;
SDL_GPUTexture* scene_texture_ = nullptr;
SDL_GPUTexture* scaled_texture_ = nullptr;
SDL_GPUTexture* postfx_texture_ = nullptr;
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
SDL_GPUSampler* sampler_ = nullptr;
SDL_GPUSampler* linear_sampler_ = nullptr;
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F, .pixel_scale = 1.0F, .oversample = 1.0F};
CrtPiUniforms crtpi_uniforms_{.scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = 1, .enable_multisample = 1, .enable_gamma = 1};
ShaderType active_shader_ = ShaderType::POSTFX;
int game_width_ = 0;
int game_height_ = 0;
int ss_factor_ = 0;
int oversample_ = 1;
int downscale_algo_ = 1;
std::string driver_name_;
std::string preferred_driver_;
bool is_initialized_ = false;
bool vsync_ = true;
bool integer_scale_ = false;
bool linear_upscale_ = false;
};
} // namespace Rendering

View File

@@ -0,0 +1,633 @@
#pragma once
#include <cstddef>
#include <cstdint>
static const uint8_t kupscale_frag_spv[] = {
0x03,
0x02,
0x23,
0x07,
0x00,
0x00,
0x01,
0x00,
0x0b,
0x00,
0x0d,
0x00,
0x14,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x11,
0x00,
0x02,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x06,
0x00,
0x01,
0x00,
0x00,
0x00,
0x47,
0x4c,
0x53,
0x4c,
0x2e,
0x73,
0x74,
0x64,
0x2e,
0x34,
0x35,
0x30,
0x00,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x03,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x07,
0x00,
0x04,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x6d,
0x61,
0x69,
0x6e,
0x00,
0x00,
0x00,
0x00,
0x09,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x10,
0x00,
0x03,
0x00,
0x04,
0x00,
0x00,
0x00,
0x07,
0x00,
0x00,
0x00,
0x03,
0x00,
0x03,
0x00,
0x02,
0x00,
0x00,
0x00,
0xc2,
0x01,
0x00,
0x00,
0x04,
0x00,
0x0a,
0x00,
0x47,
0x4c,
0x5f,
0x47,
0x4f,
0x4f,
0x47,
0x4c,
0x45,
0x5f,
0x63,
0x70,
0x70,
0x5f,
0x73,
0x74,
0x79,
0x6c,
0x65,
0x5f,
0x6c,
0x69,
0x6e,
0x65,
0x5f,
0x64,
0x69,
0x72,
0x65,
0x63,
0x74,
0x69,
0x76,
0x65,
0x00,
0x00,
0x04,
0x00,
0x08,
0x00,
0x47,
0x4c,
0x5f,
0x47,
0x4f,
0x4f,
0x47,
0x4c,
0x45,
0x5f,
0x69,
0x6e,
0x63,
0x6c,
0x75,
0x64,
0x65,
0x5f,
0x64,
0x69,
0x72,
0x65,
0x63,
0x74,
0x69,
0x76,
0x65,
0x00,
0x05,
0x00,
0x04,
0x00,
0x04,
0x00,
0x00,
0x00,
0x6d,
0x61,
0x69,
0x6e,
0x00,
0x00,
0x00,
0x00,
0x05,
0x00,
0x05,
0x00,
0x09,
0x00,
0x00,
0x00,
0x6f,
0x75,
0x74,
0x5f,
0x63,
0x6f,
0x6c,
0x6f,
0x72,
0x00,
0x00,
0x00,
0x05,
0x00,
0x04,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x73,
0x63,
0x65,
0x6e,
0x65,
0x00,
0x00,
0x00,
0x05,
0x00,
0x04,
0x00,
0x11,
0x00,
0x00,
0x00,
0x76,
0x5f,
0x75,
0x76,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x09,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x21,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x22,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x11,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x13,
0x00,
0x02,
0x00,
0x02,
0x00,
0x00,
0x00,
0x21,
0x00,
0x03,
0x00,
0x03,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x16,
0x00,
0x03,
0x00,
0x06,
0x00,
0x00,
0x00,
0x20,
0x00,
0x00,
0x00,
0x17,
0x00,
0x04,
0x00,
0x07,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x08,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x07,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x08,
0x00,
0x00,
0x00,
0x09,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x19,
0x00,
0x09,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x1b,
0x00,
0x03,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x0c,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x0c,
0x00,
0x00,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x17,
0x00,
0x04,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x10,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x10,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x36,
0x00,
0x05,
0x00,
0x02,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0xf8,
0x00,
0x02,
0x00,
0x05,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x12,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x57,
0x00,
0x05,
0x00,
0x07,
0x00,
0x00,
0x00,
0x13,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x12,
0x00,
0x00,
0x00,
0x3e,
0x00,
0x03,
0x00,
0x09,
0x00,
0x00,
0x00,
0x13,
0x00,
0x00,
0x00,
0xfd,
0x00,
0x01,
0x00,
0x38,
0x00,
0x01,
0x00};
static const size_t kupscale_frag_spv_size = 628;

View File

@@ -0,0 +1,88 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include <utility>
namespace Rendering {
/** @brief Identificador del shader de post-procesado activo */
enum class ShaderType { POSTFX,
CRTPI };
/**
* @brief Parámetros de intensidad de los efectos PostFX
*/
struct PostFXParams {
float vignette = 0.0F;
float scanlines = 0.0F;
float chroma = 0.0F;
float mask = 0.0F;
float gamma = 0.0F;
float curvature = 0.0F;
float bleeding = 0.0F;
float flicker = 0.0F;
};
/**
* @brief Parámetros del shader CRT-Pi (algoritmo de scanlines continuas)
*/
struct CrtPiParams {
float scanline_weight{6.0F};
float scanline_gap_brightness{0.12F};
float bloom_factor{3.5F};
float input_gamma{2.4F};
float output_gamma{2.2F};
float mask_brightness{0.80F};
float curvature_x{0.05F};
float curvature_y{0.10F};
int mask_type{2};
bool enable_scanlines{true};
bool enable_multisample{true};
bool enable_gamma{true};
bool enable_curvature{false};
bool enable_sharper{false};
};
/**
* @brief Interfaz abstracta para backends de renderizado con shaders
*/
class ShaderBackend {
public:
virtual ~ShaderBackend() = default;
virtual auto init(SDL_Window* window,
SDL_Texture* texture,
const std::string& vertex_source,
const std::string& fragment_source) -> bool = 0;
virtual void render() = 0;
virtual void setTextureSize(float width, float height) = 0;
virtual void cleanup() = 0;
virtual void uploadPixels(const Uint32* /*pixels*/, int /*width*/, int /*height*/) {}
virtual void setPostFXParams(const PostFXParams& /*p*/) {}
virtual void setVSync(bool /*vsync*/) {}
virtual void setScaleMode(bool /*integer_scale*/) {}
virtual void setOversample(int /*factor*/) {}
virtual void setLinearUpscale(bool /*linear*/) {}
[[nodiscard]] virtual auto isLinearUpscale() const -> bool { return false; }
virtual void setDownscaleAlgo(int /*algo*/) {}
[[nodiscard]] virtual auto getDownscaleAlgo() const -> int { return 0; }
[[nodiscard]] virtual auto getSsTextureSize() const -> std::pair<int, int> { return {0, 0}; }
[[nodiscard]] virtual auto isHardwareAccelerated() const -> bool = 0;
[[nodiscard]] virtual auto getDriverName() const -> std::string { return {}; }
virtual void setPreferredDriver(const std::string& /*driver*/) {}
virtual void setActiveShader(ShaderType /*type*/) {}
virtual void setCrtPiParams(const CrtPiParams& /*p*/) {}
[[nodiscard]] virtual auto getActiveShader() const -> ShaderType { return ShaderType::POSTFX; }
};
} // namespace Rendering

View File

@@ -0,0 +1,304 @@
#include "animated_sprite.hpp"
#include <SDL3/SDL.h> // Para SDL_LogWarn, SDL_LogCategory, SDL_LogError, SDL_FRect
#include <algorithm> // Para min
#include <cstddef> // Para size_t
#include <fstream> // Para basic_istream, basic_ifstream, istream, basic_ios, ifstream, istringstream, stringstream
#include <iostream> // Para std::cout
#include <sstream> // Para basic_istringstream, basic_stringstream
#include <stdexcept> // Para runtime_error
#include <utility> // Para move, pair
#include "resource_helper.hpp" // Para loadFile
#include "texture.hpp" // Para Texture
// Carga las animaciones en un vector(Animations) desde un fichero
auto loadAnimationsFromFile(const std::string& file_path) -> AnimationsFileBuffer {
// Intentar cargar desde ResourceHelper primero
auto resource_data = ResourceHelper::loadFile(file_path);
std::istringstream stream;
bool using_resource_data = false;
if (!resource_data.empty()) {
std::string content(resource_data.begin(), resource_data.end());
stream.str(content);
using_resource_data = true;
}
// Fallback a archivo directo
std::ifstream file;
if (!using_resource_data) {
file.open(file_path);
if (!file) {
std::cout << "Error: Fichero no encontrado " << file_path << '\n';
throw std::runtime_error("Fichero no encontrado: " + file_path);
}
}
std::istream& input_stream = using_resource_data ? stream : static_cast<std::istream&>(file);
std::vector<std::string> buffer;
std::string line;
while (std::getline(input_stream, line)) {
// Eliminar caracteres de retorno de carro (\r) al final de la línea
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
if (!line.empty()) {
buffer.push_back(line);
}
}
return buffer;
}
// Constructor
AnimatedSprite::AnimatedSprite(std::shared_ptr<Texture> texture, const std::string& file_path)
: MovingSprite(std::move(texture)) {
// Carga las animaciones
if (!file_path.empty()) {
auto buffer = loadAnimationsFromFile(file_path);
loadFromAnimationsFileBuffer(buffer);
}
}
// Constructor
AnimatedSprite::AnimatedSprite(std::shared_ptr<Texture> texture, const AnimationsFileBuffer& animations)
: MovingSprite(std::move(texture)) {
if (!animations.empty()) {
loadFromAnimationsFileBuffer(animations);
}
}
// Obtiene el índice de la animación a partir del nombre
auto AnimatedSprite::getAnimationIndex(const std::string& name) -> int {
auto iterator = animation_indices_.find(name);
if (iterator != animation_indices_.end()) {
// Si se encuentra la animación en el mapa, devuelve su índice
return iterator->second;
}
// Si no se encuentra, muestra una advertencia y devuelve -1
std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n';
return -1;
}
// Calcula el frame correspondiente a la animación (time-based)
void AnimatedSprite::animate(float delta_time) {
if (animations_[current_animation_].speed == 0 || animations_[current_animation_].paused) {
return;
}
// Acumular tiempo transcurrido
animations_[current_animation_].time_accumulator += delta_time;
// Verificar si es momento de cambiar frame
if (animations_[current_animation_].time_accumulator >= animations_[current_animation_].speed) {
animations_[current_animation_].time_accumulator -= animations_[current_animation_].speed;
animations_[current_animation_].current_frame++;
// Si alcanza el final de la animación
if (animations_[current_animation_].current_frame >= animations_[current_animation_].frames.size()) {
if (animations_[current_animation_].loop == -1) { // Si no hay loop, deja el último frame
animations_[current_animation_].current_frame = animations_[current_animation_].frames.size() - 1;
animations_[current_animation_].completed = true;
} else { // Si hay loop, vuelve al frame indicado
animations_[current_animation_].time_accumulator = 0.0F;
animations_[current_animation_].current_frame = animations_[current_animation_].loop;
}
}
// Actualizar el sprite clip
updateSpriteClip();
}
}
// Comprueba si ha terminado la animación
auto AnimatedSprite::animationIsCompleted() -> bool {
return animations_[current_animation_].completed;
}
// Establece la animacion actual
void AnimatedSprite::setCurrentAnimation(const std::string& name, bool reset) {
const auto NEW_ANIMATION = getAnimationIndex(name);
if (current_animation_ != NEW_ANIMATION) {
const auto OLD_ANIMATION = current_animation_;
current_animation_ = NEW_ANIMATION;
if (reset) {
animations_[current_animation_].current_frame = 0;
animations_[current_animation_].time_accumulator = 0.0F;
animations_[current_animation_].completed = false;
} else {
animations_[current_animation_].current_frame = std::min(animations_[OLD_ANIMATION].current_frame, animations_[current_animation_].frames.size() - 1);
animations_[current_animation_].time_accumulator = animations_[OLD_ANIMATION].time_accumulator;
animations_[current_animation_].completed = animations_[OLD_ANIMATION].completed;
}
updateSpriteClip();
}
}
// Establece la animacion actual
void AnimatedSprite::setCurrentAnimation(int index, bool reset) {
const auto NEW_ANIMATION = index;
if (current_animation_ != NEW_ANIMATION) {
const auto OLD_ANIMATION = current_animation_;
current_animation_ = NEW_ANIMATION;
if (reset) {
animations_[current_animation_].current_frame = 0;
animations_[current_animation_].time_accumulator = 0.0F;
animations_[current_animation_].completed = false;
} else {
animations_[current_animation_].current_frame = std::min(animations_[OLD_ANIMATION].current_frame, animations_[current_animation_].frames.size());
animations_[current_animation_].time_accumulator = animations_[OLD_ANIMATION].time_accumulator;
animations_[current_animation_].completed = animations_[OLD_ANIMATION].completed;
}
updateSpriteClip();
}
}
// Actualiza las variables del objeto (time-based)
void AnimatedSprite::update(float delta_time) {
animate(delta_time);
MovingSprite::update(delta_time);
}
// Reinicia la animación
void AnimatedSprite::resetAnimation() {
animations_[current_animation_].current_frame = 0;
animations_[current_animation_].time_accumulator = 0.0F;
animations_[current_animation_].completed = false;
animations_[current_animation_].paused = false;
updateSpriteClip();
}
// Carga la animación desde un vector de cadenas
void AnimatedSprite::loadFromAnimationsFileBuffer(const AnimationsFileBuffer& source) {
AnimationConfig config;
size_t index = 0;
while (index < source.size()) {
const std::string& line = source.at(index);
if (line == "[animation]") {
index = processAnimationBlock(source, index, config);
} else {
processConfigLine(line, config);
}
index++;
}
// Pone un valor por defecto
setWidth(config.frame_width);
setHeight(config.frame_height);
// Establece el primer frame inmediatamente si hay animaciones
if (!animations_.empty()) {
current_animation_ = 0;
updateSpriteClip();
}
}
// Procesa una línea de configuración
void AnimatedSprite::processConfigLine(const std::string& line, AnimationConfig& config) {
size_t pos = line.find('=');
if (pos == std::string::npos) {
return;
}
std::string key = line.substr(0, pos);
int value = std::stoi(line.substr(pos + 1));
if (key == "frame_width") {
config.frame_width = value;
updateFrameCalculations(config);
} else if (key == "frame_height") {
config.frame_height = value;
updateFrameCalculations(config);
} else {
std::cout << "Warning: unknown parameter " << key << '\n';
}
}
// Actualiza los cálculos basados en las dimensiones del frame
void AnimatedSprite::updateFrameCalculations(AnimationConfig& config) {
config.frames_per_row = getTexture()->getWidth() / config.frame_width;
const int WIDTH = getTexture()->getWidth() / config.frame_width;
const int HEIGHT = getTexture()->getHeight() / config.frame_height;
config.max_tiles = WIDTH * HEIGHT;
}
// Procesa un bloque completo de animación
auto AnimatedSprite::processAnimationBlock(const AnimationsFileBuffer& source, size_t start_index, const AnimationConfig& config) -> size_t {
Animation animation;
size_t index = start_index + 1; // Salta la línea "[animation]"
while (index < source.size()) {
const std::string& line = source.at(index);
if (line == "[/animation]") {
break;
}
processAnimationParameter(line, animation, config);
index++;
}
// Añade la animación al vector de animaciones
animations_.emplace_back(animation);
// Rellena el mapa con el nombre y el nuevo índice
animation_indices_[animation.name] = animations_.size() - 1;
return index;
}
// Procesa un parámetro individual de animación
void AnimatedSprite::processAnimationParameter(const std::string& line, Animation& animation, const AnimationConfig& config) {
size_t pos = line.find('=');
if (pos == std::string::npos) {
return;
}
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + 1);
if (key == "name") {
animation.name = value;
} else if (key == "speed") {
animation.speed = std::stof(value);
} else if (key == "loop") {
animation.loop = std::stoi(value);
} else if (key == "frames") {
parseFramesParameter(value, animation, config);
} else {
std::cout << "Warning: unknown parameter " << key << '\n';
}
}
// Parsea el parámetro de frames (lista separada por comas)
void AnimatedSprite::parseFramesParameter(const std::string& frames_str, Animation& animation, const AnimationConfig& config) {
std::stringstream stream(frames_str);
std::string tmp;
SDL_FRect rect = {.x = 0, .y = 0, .w = config.frame_width, .h = config.frame_height};
while (getline(stream, tmp, ',')) {
const int NUM_TILE = std::stoi(tmp);
if (NUM_TILE <= config.max_tiles) {
rect.x = (NUM_TILE % config.frames_per_row) * config.frame_width;
rect.y = (NUM_TILE / config.frames_per_row) * config.frame_height;
animation.frames.emplace_back(rect);
}
}
}
// Establece la velocidad de la animación
void AnimatedSprite::setAnimationSpeed(float value) {
animations_[current_animation_].speed = value;
}
// Actualiza el spriteClip con el frame correspondiente
void AnimatedSprite::updateSpriteClip() {
setSpriteClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
}

View File

@@ -0,0 +1,88 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect
#include <cstddef> // Para size_t
#include <memory> // Para shared_ptr
#include <string> // Para basic_string, string, hash
#include <unordered_map> // Para unordered_map
#include <utility> // Para move
#include <vector> // Para vector
#include "moving_sprite.hpp" // for MovingSprite
// Declaración adelantada
class Texture;
// --- Estructuras ---
struct Animation {
static constexpr float DEFAULT_SPEED = 80.0F;
std::string name; // Nombre de la animación
std::vector<SDL_FRect> frames; // Frames que componen la animación
float speed{DEFAULT_SPEED}; // Velocidad de reproducción (ms entre frames)
int loop{0}; // Frame de vuelta al terminar (-1 para no repetir)
bool completed{false}; // Indica si la animación ha finalizado
size_t current_frame{0}; // Frame actual en reproducción
float time_accumulator{0.0F}; // Acumulador de tiempo para animaciones time-based
bool paused{false}; // La animación no avanza
Animation() = default;
};
struct AnimationConfig {
float frame_width = 1.0F;
float frame_height = 1.0F;
int frames_per_row = 1;
int max_tiles = 1;
};
// --- Tipos ---
using AnimationsFileBuffer = std::vector<std::string>; // Buffer de animaciones
// --- Funciones ---
auto loadAnimationsFromFile(const std::string& file_path) -> AnimationsFileBuffer; // Carga las animaciones desde un fichero
// --- Clase AnimatedSprite: sprite animado que hereda de MovingSprite ---
class AnimatedSprite : public MovingSprite {
public:
// --- Constructores y destructor ---
AnimatedSprite(std::shared_ptr<Texture> texture, const std::string& file_path);
AnimatedSprite(std::shared_ptr<Texture> texture, const AnimationsFileBuffer& animations);
explicit AnimatedSprite(std::shared_ptr<Texture> texture)
: MovingSprite(std::move(texture)) {}
~AnimatedSprite() override = default;
// --- Métodos principales ---
void update(float delta_time) override; // Actualiza la animación (time-based)
// --- Control de animaciones ---
void setCurrentAnimation(const std::string& name = "default", bool reset = true); // Establece la animación por nombre
void setCurrentAnimation(int index = 0, bool reset = true); // Establece la animación por índice
void resetAnimation(); // Reinicia la animación actual
void setAnimationSpeed(float value); // Establece la velocidad de la animación
[[nodiscard]] auto getAnimationSpeed() const -> float { return animations_[current_animation_].speed; } // Obtiene la velocidad de la animación actual
void animtionPause() { animations_[current_animation_].paused = true; } // Detiene la animación
void animationResume() { animations_[current_animation_].paused = false; } // Reanuda la animación
[[nodiscard]] auto getCurrentAnimationFrame() const -> size_t { return animations_[current_animation_].current_frame; } // Obtiene el numero de frame de la animación actual
// --- Consultas ---
auto animationIsCompleted() -> bool; // Comprueba si la animación ha terminado
auto getAnimationIndex(const std::string& name) -> int; // Obtiene el índice de una animación por nombre
protected:
// --- Variables de estado ---
std::vector<Animation> animations_; // Vector de animaciones disponibles
std::unordered_map<std::string, int> animation_indices_; // Mapa para búsqueda rápida por nombre
int current_animation_ = 0; // Índice de la animación activa
// --- Métodos internos ---
void animate(float delta_time); // Calcula el frame correspondiente a la animación (time-based)
void loadFromAnimationsFileBuffer(const AnimationsFileBuffer& source); // Carga la animación desde un vector de cadenas
void processConfigLine(const std::string& line, AnimationConfig& config); // Procesa una línea de configuración
void updateFrameCalculations(AnimationConfig& config); // Actualiza los cálculos basados en las dimensiones del frame
auto processAnimationBlock(const AnimationsFileBuffer& source, size_t start_index, const AnimationConfig& config) -> size_t; // Procesa un bloque completo de animación
static void processAnimationParameter(const std::string& line, Animation& animation, const AnimationConfig& config); // Procesa un parámetro individual de animación
static void parseFramesParameter(const std::string& frames_str, Animation& animation, const AnimationConfig& config); // Parsea el parámetro de frames (lista separada por comas)
void updateSpriteClip(); // Actualiza el spriteClip con el frame correspondiente
};

View File

@@ -0,0 +1,255 @@
#include "card_sprite.hpp"
#include <algorithm> // Para std::clamp
#include <functional> // Para function
#include <utility> // Para move
#include "texture.hpp" // Para Texture
#include "utils.hpp" // Para easeOutBounce, easeOutCubic
// Constructor
CardSprite::CardSprite(std::shared_ptr<Texture> texture)
: MovingSprite(std::move(texture)),
entry_easing_(easeOutBounce) {}
// Inicia la animación de entrada (solo si está en IDLE)
auto CardSprite::enable() -> bool {
if (state_ != CardState::IDLE) {
return false;
}
state_ = CardState::ENTERING;
entry_elapsed_ = 0.0F;
first_touch_ = false;
// Posición inicial (borde de pantalla)
setPos(entry_start_x_, entry_start_y_);
// Zoom inicial grande (como si estuviera cerca de la cámara)
horizontal_zoom_ = start_zoom_;
vertical_zoom_ = start_zoom_;
// Ángulo inicial
rotate_.angle = start_angle_;
rotate_.center = {pos_.w / 2.0F, pos_.h / 2.0F};
shadow_visible_ = true;
return true;
}
// Inicia la animación de salida (solo si está en LANDED)
void CardSprite::startExit() {
if (state_ != CardState::LANDED) {
return;
}
state_ = CardState::EXITING;
shadow_visible_ = true;
// Velocidad y aceleración de salida
vx_ = exit_vx_;
vy_ = exit_vy_;
ax_ = exit_ax_;
ay_ = exit_ay_;
// Rotación continua
rotate_.enabled = true;
rotate_.amount = exit_rotate_amount_;
rotate_.center = {pos_.w / 2.0F, pos_.h / 2.0F};
}
// Actualiza según el estado
void CardSprite::update(float delta_time) {
switch (state_) {
case CardState::ENTERING:
updateEntering(delta_time);
break;
case CardState::EXITING:
updateExiting(delta_time);
break;
default:
break;
}
}
// Animación de entrada: interpola posición, zoom y ángulo
void CardSprite::updateEntering(float delta_time) {
entry_elapsed_ += delta_time;
float progress = std::clamp(entry_elapsed_ / entry_duration_s_, 0.0F, 1.0F);
double eased = entry_easing_(static_cast<double>(progress));
// Zoom: de start_zoom_ a 1.0 con rebote
auto current_zoom = static_cast<float>(start_zoom_ + (1.0 - start_zoom_) * eased);
horizontal_zoom_ = current_zoom;
vertical_zoom_ = current_zoom;
// Ángulo: de start_angle_ a 0 con rebote
rotate_.angle = start_angle_ * (1.0 - eased);
// Posición: de entry_start a landing con easing suave (sin rebote)
// Usamos easeOutCubic para que el desplazamiento sea fluido
double pos_eased = easeOutCubic(static_cast<double>(progress));
auto current_x = static_cast<float>(entry_start_x_ + (landing_x_ - entry_start_x_) * pos_eased);
auto current_y = static_cast<float>(entry_start_y_ + (landing_y_ - entry_start_y_) * pos_eased);
setPos(current_x, current_y);
// Detecta el primer toque (cuando el easing alcanza ~1.0 por primera vez)
if (!first_touch_ && eased >= FIRST_TOUCH_THRESHOLD) {
first_touch_ = true;
}
// Transición a LANDED cuando termina la animación completa
if (progress >= 1.0F) {
horizontal_zoom_ = 1.0F;
vertical_zoom_ = 1.0F;
rotate_.angle = 0.0;
setPos(landing_x_, landing_y_);
state_ = CardState::LANDED;
first_touch_ = true;
}
}
// Animación de salida: movimiento + rotación continua + zoom opcional
void CardSprite::updateExiting(float delta_time) {
move(delta_time);
rotate(delta_time);
// Ganar altura gradualmente (zoom hacia el objetivo)
if (exit_zoom_speed_ > 0.0F && horizontal_zoom_ < exit_target_zoom_) {
float new_zoom = horizontal_zoom_ + exit_zoom_speed_ * delta_time;
if (new_zoom > exit_target_zoom_) {
new_zoom = exit_target_zoom_;
}
horizontal_zoom_ = new_zoom;
vertical_zoom_ = new_zoom;
}
if (isOffScreen()) {
state_ = CardState::FINISHED;
}
}
// Renderiza el sprite y su sombra
void CardSprite::render() {
if (state_ == CardState::IDLE || state_ == CardState::FINISHED) {
return;
}
// Sombra primero (debajo de la tarjeta)
if (shadow_visible_ && shadow_texture_) {
renderShadow();
}
// Tarjeta
MovingSprite::render();
}
// Renderiza la sombra con efecto de perspectiva 2D→3D (efecto helicóptero)
//
// Fuente de luz en la esquina superior izquierda (0,0).
// La sombra se mueve con la tarjeta pero desplazada en dirección opuesta a la luz
// (abajo-derecha a 45°). Cuanto más alta la tarjeta (zoom > 1.0):
// - Más separada de la tarjeta (offset grande)
// - Más pequeña (proyección lejana)
// Cuando la tarjeta está en la mesa (zoom=1.0):
// - Sombra pegada con offset base
// - Tamaño real
void CardSprite::renderShadow() {
// Altura sobre la mesa: 0.0 = en la mesa, 0.8 = alta (zoom 1.8)
float height = horizontal_zoom_ - 1.0F;
// Escala: más pequeña cuanto más alta
float shadow_zoom = 1.0F / horizontal_zoom_;
// Offset respecto a la tarjeta: base + extra proporcional a la altura
// La sombra se aleja en diagonal abajo-derecha (opuesta a la luz en 0,0)
float offset_x = shadow_offset_x_ + height * SHADOW_HEIGHT_MULTIPLIER;
float offset_y = shadow_offset_y_ + height * SHADOW_HEIGHT_MULTIPLIER;
shadow_texture_->render(
pos_.x + offset_x,
pos_.y + offset_y,
&sprite_clip_,
shadow_zoom,
shadow_zoom,
rotate_.angle,
&rotate_.center,
flip_);
}
// Comprueba si el sprite está fuera de pantalla
auto CardSprite::isOffScreen() const -> bool {
float effective_width = pos_.w * horizontal_zoom_;
float effective_height = pos_.h * vertical_zoom_;
return (pos_.x + effective_width < -OFF_SCREEN_MARGIN ||
pos_.x > screen_width_ + OFF_SCREEN_MARGIN ||
pos_.y + effective_height < -OFF_SCREEN_MARGIN ||
pos_.y > screen_height_ + OFF_SCREEN_MARGIN);
}
// --- Consultas de estado ---
auto CardSprite::hasLanded() const -> bool {
return state_ == CardState::LANDED || state_ == CardState::EXITING || state_ == CardState::FINISHED;
}
auto CardSprite::hasFirstTouch() const -> bool {
return first_touch_;
}
auto CardSprite::hasFinished() const -> bool {
return state_ == CardState::FINISHED;
}
auto CardSprite::isExiting() const -> bool {
return state_ == CardState::EXITING;
}
auto CardSprite::getState() const -> CardState {
return state_;
}
// --- Configuración ---
void CardSprite::setEntryParams(float start_zoom, double start_angle, float duration_s, std::function<double(double)> easing) {
start_zoom_ = start_zoom;
start_angle_ = start_angle;
entry_duration_s_ = duration_s;
entry_easing_ = std::move(easing);
}
void CardSprite::setEntryPosition(float start_x, float start_y) {
entry_start_x_ = start_x;
entry_start_y_ = start_y;
}
void CardSprite::setLandingPosition(float x, float y) {
landing_x_ = x;
landing_y_ = y;
}
void CardSprite::setExitParams(float vx, float vy, float ax, float ay, double rotate_amount) {
exit_vx_ = vx;
exit_vy_ = vy;
exit_ax_ = ax;
exit_ay_ = ay;
exit_rotate_amount_ = rotate_amount;
}
void CardSprite::setExitLift(float target_zoom, float zoom_speed) {
exit_target_zoom_ = target_zoom;
exit_zoom_speed_ = zoom_speed;
}
void CardSprite::setShadowTexture(std::shared_ptr<Texture> texture) {
shadow_texture_ = std::move(texture);
}
void CardSprite::setShadowOffset(float offset_x, float offset_y) {
shadow_offset_x_ = offset_x;
shadow_offset_y_ = offset_y;
}
void CardSprite::setScreenBounds(float width, float height) {
screen_width_ = width;
screen_height_ = height;
}

View File

@@ -0,0 +1,109 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FPoint
#include <functional> // Para function
#include <memory> // Para shared_ptr
#include "moving_sprite.hpp" // Para MovingSprite
class Texture;
// --- Estados de la tarjeta ---
enum class CardState {
IDLE, // No activada todavía
ENTERING, // Animación de entrada (zoom + rotación + desplazamiento con rebote)
LANDED, // En reposo sobre la mesa
EXITING, // Saliendo de pantalla girando
FINISHED, // Fuera de pantalla
};
// --- Clase CardSprite: tarjeta animada con zoom, rotación y sombra integrada ---
//
// Simula una tarjeta lanzada sobre una mesa desde un borde de la pantalla.
// Durante la entrada, interpola posición, zoom y rotación con easing (rebote).
// Durante la salida, se desplaza fuera de pantalla girando, sin sombra.
class CardSprite : public MovingSprite {
public:
explicit CardSprite(std::shared_ptr<Texture> texture);
~CardSprite() override = default;
// --- Ciclo principal ---
void update(float delta_time) override;
void render() override;
// --- Control de estado ---
auto enable() -> bool; // Inicia la animación de entrada (true si se activó)
void startExit(); // Inicia la animación de salida
// --- Consultas de estado ---
[[nodiscard]] auto hasLanded() const -> bool; // ¿Ha aterrizado definitivamente?
[[nodiscard]] auto hasFirstTouch() const -> bool; // ¿Ha tocado la mesa por primera vez? (primer rebote)
[[nodiscard]] auto hasFinished() const -> bool; // ¿Ha terminado completamente?
[[nodiscard]] auto isExiting() const -> bool; // ¿Está saliendo de pantalla?
[[nodiscard]] auto getState() const -> CardState; // Estado actual
// --- Configuración de entrada ---
void setEntryParams(float start_zoom, double start_angle, float duration_s, std::function<double(double)> easing);
void setEntryPosition(float start_x, float start_y); // Posición inicial (borde de pantalla)
void setLandingPosition(float x, float y); // Posición final centrada
// --- Configuración de salida ---
void setExitParams(float vx, float vy, float ax, float ay, double rotate_amount);
void setExitLift(float target_zoom, float zoom_speed); // Ganar altura al salir (zoom > 1.0)
// --- Sombra ---
void setShadowTexture(std::shared_ptr<Texture> texture);
void setShadowOffset(float offset_x, float offset_y);
// --- Limites de pantalla (para detectar salida) ---
void setScreenBounds(float width, float height);
private:
// --- Estado ---
CardState state_ = CardState::IDLE;
bool first_touch_ = false; // Primer contacto con la mesa (eased >= umbral)
// --- Umbral para detectar el primer toque ---
static constexpr double FIRST_TOUCH_THRESHOLD = 0.98;
// --- Parámetros de entrada ---
float start_zoom_ = 1.8F;
double start_angle_ = 15.0;
float entry_duration_s_ = 1.5F;
float entry_elapsed_ = 0.0F;
std::function<double(double)> entry_easing_;
float entry_start_x_ = 0.0F; // Posición inicial X (borde)
float entry_start_y_ = 0.0F; // Posición inicial Y (borde)
float landing_x_ = 0.0F;
float landing_y_ = 0.0F;
// --- Parámetros de salida ---
float exit_vx_ = 0.0F;
float exit_vy_ = 0.0F;
float exit_ax_ = 0.0F;
float exit_ay_ = 0.0F;
double exit_rotate_amount_ = 0.0;
float exit_target_zoom_ = 1.0F; // Zoom objetivo al salir (>1.0 = se eleva)
float exit_zoom_speed_ = 0.0F; // Velocidad de cambio de zoom por segundo
// --- Sombra ---
std::shared_ptr<Texture> shadow_texture_;
float shadow_offset_x_ = 8.0F;
float shadow_offset_y_ = 8.0F;
bool shadow_visible_ = true;
// --- Límites de pantalla ---
float screen_width_ = 320.0F;
float screen_height_ = 240.0F;
// --- Constantes ---
static constexpr float OFF_SCREEN_MARGIN = 50.0F; // Margen fuera de pantalla para considerar FINISHED
static constexpr float SHADOW_HEIGHT_MULTIPLIER = 400.0F; // Pixels de separación de sombra por unidad de altura
// --- Métodos internos ---
void updateEntering(float delta_time);
void updateExiting(float delta_time);
void renderShadow();
[[nodiscard]] auto isOffScreen() const -> bool;
};

View File

@@ -0,0 +1,136 @@
#include "moving_sprite.hpp"
#include <cmath> // Para std::abs
#include <utility>
#include "texture.hpp" // Para Texture
// Constructor
MovingSprite::MovingSprite(std::shared_ptr<Texture> texture, SDL_FRect pos, Rotate rotate, float horizontal_zoom, float vertical_zoom, SDL_FlipMode flip)
: Sprite(std::move(texture), pos),
rotate_(rotate),
flip_(flip),
x_(pos.x),
y_(pos.y),
horizontal_zoom_(horizontal_zoom),
vertical_zoom_(vertical_zoom) {}
MovingSprite::MovingSprite(std::shared_ptr<Texture> texture, SDL_FRect pos)
: Sprite(std::move(texture), pos),
flip_(SDL_FLIP_NONE),
x_(pos.x),
y_(pos.y),
horizontal_zoom_(1.0F),
vertical_zoom_(1.0F) {}
MovingSprite::MovingSprite(std::shared_ptr<Texture> texture)
: Sprite(std::move(texture)),
flip_(SDL_FLIP_NONE),
horizontal_zoom_(1.0F),
vertical_zoom_(1.0F) { Sprite::clear(); }
// Reinicia todas las variables
void MovingSprite::clear() {
stop();
Sprite::clear();
}
// Elimina el movimiento del sprite
void MovingSprite::stop() {
x_ = 0.0F; // Posición en el eje X
y_ = 0.0F; // Posición en el eje Y
vx_ = 0.0F; // Velocidad en el eje X. Cantidad de pixeles a desplazarse
vy_ = 0.0F; // Velocidad en el eje Y. Cantidad de pixeles a desplazarse
ax_ = 0.0F; // Aceleración en el eje X. Variación de la velocidad
ay_ = 0.0F; // Aceleración en el eje Y. Variación de la velocidad
rotate_ = Rotate(); // Inicializa la estructura
horizontal_zoom_ = 1.0F; // Zoom aplicado a la anchura
vertical_zoom_ = 1.0F; // Zoom aplicado a la altura
flip_ = SDL_FLIP_NONE; // Establece como se ha de voltear el sprite
}
// Mueve el sprite (time-based)
void MovingSprite::move(float delta_time) {
// DeltaTime puro: velocidad (pixels/ms) * tiempo (ms)
x_ += vx_ * delta_time;
y_ += vy_ * delta_time;
// Aceleración (pixels/ms²) * tiempo (ms)
vx_ += ax_ * delta_time;
vy_ += ay_ * delta_time;
pos_.x = static_cast<int>(x_);
pos_.y = static_cast<int>(y_);
}
// Actualiza las variables internas del objeto (time-based)
void MovingSprite::update(float delta_time) {
move(delta_time);
rotate(delta_time);
}
// Muestra el sprite por pantalla
void MovingSprite::render() {
getTexture()->render(pos_.x, pos_.y, &sprite_clip_, horizontal_zoom_, vertical_zoom_, rotate_.angle, &rotate_.center, flip_);
}
// Establece la rotacion (time-based)
void MovingSprite::rotate(float delta_time) {
if (rotate_.enabled) {
rotate_.angle += rotate_.amount * delta_time;
}
}
// Activa o desactiva el efecto de rotación
void MovingSprite::setRotate(bool enable) {
rotate_.enabled = enable;
}
// Habilita la rotación y establece el centro en el centro del sprite
void MovingSprite::startRotate() {
rotate_.enabled = true;
rotate_.center.x = pos_.w / 2.0F;
rotate_.center.y = pos_.h / 2.0F;
}
// Detiene la rotación y resetea el ángulo a cero
void MovingSprite::stopRotate(float threshold) {
if (threshold == 0.0F || std::abs(rotate_.amount) <= threshold) {
rotate_.enabled = false;
rotate_.angle = 0.0;
}
}
// Establece la posición y_ el tamaño del objeto
void MovingSprite::setPos(SDL_FRect rect) {
x_ = rect.x;
y_ = rect.y;
pos_ = rect;
}
// Establece el valor de las variables
void MovingSprite::setPos(float pos_x, float pos_y) {
x_ = pos_x;
y_ = pos_y;
pos_.x = static_cast<int>(x_);
pos_.y = static_cast<int>(y_);
}
// Establece el valor de la variable
void MovingSprite::setPosX(float pos_x) {
x_ = pos_x;
pos_.x = static_cast<int>(x_);
}
// Establece el valor de la variable
void MovingSprite::setPosY(float pos_y) {
y_ = pos_y;
pos_.y = static_cast<int>(y_);
}

View File

@@ -0,0 +1,83 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FlipMode, SDL_FPoint, SDL_FRect
#include <memory> // Para shared_ptr
#include "sprite.hpp" // for Sprite
class Texture;
// --- Clase MovingSprite: añade movimiento y efectos de rotación, zoom y flip al sprite ---
class MovingSprite : public Sprite {
public:
// --- Estructuras ---
struct Rotate {
bool enabled{false}; // Indica si ha de rotar
double angle{0.0}; // Ángulo para dibujarlo
float amount{0.0F}; // Cantidad de grados a girar en cada iteración
SDL_FPoint center{.x = 0.0F, .y = 0.0F}; // Centro de rotación
};
// --- Constructores y destructor ---
MovingSprite(std::shared_ptr<Texture> texture, SDL_FRect pos, MovingSprite::Rotate rotate, float horizontal_zoom, float vertical_zoom, SDL_FlipMode flip);
MovingSprite(std::shared_ptr<Texture> texture, SDL_FRect pos);
explicit MovingSprite(std::shared_ptr<Texture> texture);
~MovingSprite() override = default;
// --- Métodos principales ---
virtual void update(float delta_time); // Actualiza las variables internas del objeto (time-based)
void clear() override; // Reinicia todas las variables a cero
void stop(); // Elimina el movimiento del sprite
void render() override; // Muestra el sprite por pantalla
// --- Configuración ---
void setPos(SDL_FRect rect); // Establece la posición y el tamaño del objeto
void setPos(float pos_x, float pos_y); // Establece la posición del objeto
void setPosX(float pos_x); // Establece la posición X
void setPosY(float pos_y); // Establece la posición Y
void setVelX(float value) { vx_ = value; } // Establece la velocidad X
void setVelY(float value) { vy_ = value; } // Establece la velocidad Y
void setAccelX(float value) { ax_ = value; } // Establece la aceleración X
void setAccelY(float value) { ay_ = value; } // Establece la aceleración Y
void setHorizontalZoom(float value) { horizontal_zoom_ = value; } // Establece el zoom horizontal
void setVerticalZoom(float value) { vertical_zoom_ = value; } // Establece el zoom vertical
void setAngle(double value) { rotate_.angle = value; } // Establece el ángulo
void setRotatingCenter(SDL_FPoint point) { rotate_.center = point; } // Establece el centro de rotación
void setRotate(bool enable); // Activa o desactiva el efecto de rotación
void startRotate(); // Habilita la rotación con centro automático
void stopRotate(float threshold = 0.0F); // Detiene la rotación y resetea ángulo
void setRotateAmount(double value) { rotate_.amount = value; } // Establece la velocidad de rotación
void scaleRotateAmount(float value) { rotate_.amount *= value; } // Modifica la velocidad de rotacion
void switchRotate() { rotate_.amount *= -1; } // Cambia el sentido de la rotación
void setFlip(SDL_FlipMode flip) { flip_ = flip; } // Establece el flip
void flip() { flip_ = (flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL; } // Cambia el flip
// --- Getters ---
[[nodiscard]] auto getPosX() const -> float { return x_; } // Obtiene la posición X
[[nodiscard]] auto getPosY() const -> float { return y_; } // Obtiene la posición Y
[[nodiscard]] auto getVelX() const -> float { return vx_; } // Obtiene la velocidad X
[[nodiscard]] auto getVelY() const -> float { return vy_; } // Obtiene la velocidad Y
[[nodiscard]] auto getAccelX() const -> float { return ax_; } // Obtiene la aceleración X
[[nodiscard]] auto getAccelY() const -> float { return ay_; } // Obtiene la aceleración Y
[[nodiscard]] auto isRotating() const -> bool { return rotate_.enabled; } // Verifica si está rotando
auto getFlip() -> SDL_FlipMode { return flip_; } // Obtiene el flip
protected:
// --- Variables de estado ---
Rotate rotate_; // Variables usadas para controlar la rotación del sprite
SDL_FlipMode flip_; // Indica cómo se voltea el sprite
float x_ = 0.0F; // Posición en el eje X
float y_ = 0.0F; // Posición en el eje Y
float vx_ = 0.0F; // Velocidad en el eje X
float vy_ = 0.0F; // Velocidad en el eje Y
float ax_ = 0.0F; // Aceleración en el eje X
float ay_ = 0.0F; // Aceleración en el eje Y
float horizontal_zoom_; // Zoom aplicado a la anchura
float vertical_zoom_; // Zoom aplicado a la altura
// --- Métodos internos ---
void updateAngle() { rotate_.angle += rotate_.amount; } // Incrementa el valor del ángulo
void move(float delta_time); // Mueve el sprite según velocidad y aceleración (time-based)
void rotate(float delta_time); // Rota el sprite según los parámetros de rotación (time-based)
};

View File

@@ -0,0 +1,233 @@
// IWYU pragma: no_include <bits/std_abs.h>
#include "path_sprite.hpp"
#include <cmath> // Para abs
#include <functional> // Para function
#include <utility> // Para move
// Constructor para paths por puntos (convertido a segundos)
Path::Path(const std::vector<SDL_FPoint>& spots_init, float waiting_time_s_init)
: spots(spots_init),
is_point_path(true) {
waiting_time_s = waiting_time_s_init;
}
// Devuelve un vector con los puntos que conforman la ruta
auto createPath(float start, float end, PathType type, float fixed_pos, int steps, const std::function<double(double)>& easing_function) -> std::vector<SDL_FPoint> {
std::vector<SDL_FPoint> v;
v.reserve(steps);
for (int i = 0; i < steps; ++i) {
double t = static_cast<double>(i) / (steps - 1);
double value = start + ((end - start) * easing_function(t));
if ((start > 0 && end < 0) || (start < 0 && end > 0)) {
value = start + ((end > 0 ? 1 : -1) * std::abs(end - start) * easing_function(t));
}
switch (type) {
case PathType::HORIZONTAL:
v.emplace_back(SDL_FPoint{.x = static_cast<float>(value), .y = fixed_pos});
break;
case PathType::VERTICAL:
v.emplace_back(SDL_FPoint{.x = fixed_pos, .y = static_cast<float>(value)});
break;
default:
break;
}
}
return v;
}
// Actualiza la posición y comprueba si ha llegado a su destino
void PathSprite::update(float delta_time) {
if (enabled_ && !has_finished_) {
moveThroughCurrentPath(delta_time);
goToNextPathOrDie();
}
}
// Muestra el sprite por pantalla
void PathSprite::render() {
if (enabled_) {
Sprite::render();
}
}
// Determina el tipo de centrado basado en el tipo de path
auto PathSprite::determineCenteringType(const Path& path, bool centered) -> PathCentered {
if (!centered) {
return PathCentered::NONE;
}
if (path.is_point_path) {
// Lógica de centrado para paths por PUNTOS
if (!path.spots.empty()) {
// Si X es constante, es un path Vertical, centramos en X
return (path.spots.back().x == path.spots.front().x) ? PathCentered::ON_X : PathCentered::ON_Y;
}
return PathCentered::NONE;
}
// Lógica de centrado para paths GENERADOS
// Si el tipo es Vertical, centramos en X
return (path.type == PathType::VERTICAL) ? PathCentered::ON_X : PathCentered::ON_Y;
}
// Aplica centrado en el eje X (para paths verticales)
void PathSprite::centerPathOnX(Path& path, float offset) {
if (path.is_point_path) {
const float X_BASE = !path.spots.empty() ? path.spots.front().x : 0.0F;
const float X = X_BASE - offset;
for (auto& spot : path.spots) {
spot.x = X;
}
} else {
// Es un path generado, ajustamos la posición fija (que es X)
path.fixed_pos -= offset;
}
}
// Aplica centrado en el eje Y (para paths horizontales)
void PathSprite::centerPathOnY(Path& path, float offset) {
if (path.is_point_path) {
const float Y_BASE = !path.spots.empty() ? path.spots.front().y : 0.0F;
const float Y = Y_BASE - offset;
for (auto& spot : path.spots) {
spot.y = Y;
}
} else {
// Es un path generado, ajustamos la posición fija (que es Y)
path.fixed_pos -= offset;
}
}
// Añade un recorrido
void PathSprite::addPath(Path path, bool centered) {
PathCentered path_centered = determineCenteringType(path, centered);
switch (path_centered) {
case PathCentered::ON_X:
centerPathOnX(path, pos_.w / 2.0F);
break;
case PathCentered::ON_Y:
centerPathOnY(path, pos_.h / 2.0F);
break;
case PathCentered::NONE:
break;
}
paths_.emplace_back(std::move(path));
}
// Añade un recorrido generado (en segundos)
void PathSprite::addPath(float start, float end, PathType type, float fixed_pos, float duration_s, const std::function<double(double)>& easing_function, float waiting_time_s) {
paths_.emplace_back(start, end, type, fixed_pos, duration_s, waiting_time_s, easing_function);
}
// Añade un recorrido por puntos (en segundos)
void PathSprite::addPath(const std::vector<SDL_FPoint>& spots, float waiting_time_s) {
paths_.emplace_back(spots, waiting_time_s);
}
// Habilita el objeto
void PathSprite::enable() {
if (paths_.empty() || enabled_) {
return;
}
enabled_ = true;
// Establece la posición inicial
auto& path = paths_.at(current_path_);
if (path.is_point_path) {
const auto& p = path.spots.at(path.counter);
setPosition(p);
} else {
// Para paths generados, establecer posición inicial
SDL_FPoint initial_pos;
if (path.type == PathType::HORIZONTAL) {
initial_pos = {.x = path.start_pos, .y = path.fixed_pos};
} else {
initial_pos = {.x = path.fixed_pos, .y = path.start_pos};
}
setPosition(initial_pos);
}
}
// Coloca el sprite en los diferentes puntos del recorrido
void PathSprite::moveThroughCurrentPath(float delta_time) {
auto& path = paths_.at(current_path_);
if (path.is_point_path) {
// Lógica para paths por puntos (compatibilidad)
const auto& p = path.spots.at(path.counter);
setPosition(p);
if (!path.on_destination) {
++path.counter;
if (std::cmp_greater_equal(path.counter, path.spots.size())) {
path.on_destination = true;
path.counter = static_cast<int>(path.spots.size()) - 1;
}
}
if (path.on_destination) {
path.waiting_elapsed += delta_time;
if (path.waiting_elapsed >= path.waiting_time_s) {
path.finished = true;
}
}
} else {
// Lógica para paths generados en tiempo real
if (!path.on_destination) {
path.elapsed_time += delta_time;
// Calcular progreso (0.0 a 1.0)
float progress = path.elapsed_time / path.duration_s;
if (progress >= 1.0F) {
progress = 1.0F;
path.on_destination = true;
}
// Aplicar función de easing
double eased_progress = path.easing_function(progress);
// Calcular posición actual
float current_pos = path.start_pos + ((path.end_pos - path.start_pos) * static_cast<float>(eased_progress));
// Establecer posición según el tipo
SDL_FPoint position;
if (path.type == PathType::HORIZONTAL) {
position = {.x = current_pos, .y = path.fixed_pos};
} else {
position = {.x = path.fixed_pos, .y = current_pos};
}
setPosition(position);
} else {
// Esperar en destino
path.waiting_elapsed += delta_time;
if (path.waiting_elapsed >= path.waiting_time_s) {
path.finished = true;
}
}
}
}
// Cambia de recorrido o finaliza
void PathSprite::goToNextPathOrDie() {
// Comprueba si ha terminado el recorrdo actual
if (paths_.at(current_path_).finished) {
++current_path_;
}
// Comprueba si quedan mas recorridos
if (std::cmp_greater_equal(current_path_, paths_.size())) {
has_finished_ = true;
current_path_ = 0;
}
}
// Indica si ha terminado todos los recorridos
auto PathSprite::hasFinished() const -> bool { return has_finished_; }

View File

@@ -0,0 +1,101 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FPoint
#include <functional> // Para std::function
#include <memory> // Para shared_ptr
#include <utility>
#include <vector> // Para vector
#include "sprite.hpp" // Para Sprite
class Texture;
// --- Enums ---
enum class PathType { // Tipos de recorrido
VERTICAL,
HORIZONTAL,
};
enum class PathCentered { // Centrado del recorrido
ON_X,
ON_Y,
NONE,
};
// --- Estructuras ---
struct Path { // Define un recorrido para el sprite
float start_pos; // Posición inicial
float end_pos; // Posición final
PathType type; // Tipo de movimiento (horizontal/vertical)
float fixed_pos; // Posición fija en el eje contrario
float duration_s; // Duración de la animación en segundos
float waiting_time_s; // Tiempo de espera una vez en el destino
std::function<double(double)> easing_function; // Función de easing
float elapsed_time = 0.0F; // Tiempo transcurrido
float waiting_elapsed = 0.0F; // Tiempo de espera transcurrido
bool on_destination = false; // Indica si ha llegado al destino
bool finished = false; // Indica si ha terminado de esperarse
// Constructor para paths generados
Path(float start, float end, PathType path_type, float fixed, float duration, float waiting, std::function<double(double)> easing)
: start_pos(start),
end_pos(end),
type(path_type),
fixed_pos(fixed),
duration_s(duration),
waiting_time_s(waiting),
easing_function(std::move(easing)) {}
// Constructor para paths por puntos (convertido a segundos)
Path(const std::vector<SDL_FPoint>& spots_init, float waiting_time_s_init);
// Variables para paths por puntos
std::vector<SDL_FPoint> spots; // Solo para paths por puntos
int counter = 0; // Solo para paths por puntos
bool is_point_path = false; // Indica si es un path por puntos
};
// --- Funciones ---
auto createPath(float start, float end, PathType type, float fixed_pos, int steps, const std::function<double(double)>& easing_function) -> std::vector<SDL_FPoint>; // Devuelve un vector con los puntos que conforman la ruta
// --- Clase PathSprite: Sprite que sigue uno o varios recorridos ---
class PathSprite : public Sprite {
public:
// --- Constructor y destructor ---
explicit PathSprite(std::shared_ptr<Texture> texture)
: Sprite(std::move(texture)) {}
~PathSprite() override = default;
// --- Métodos principales ---
void update(float delta_time); // Actualiza la posición del sprite según el recorrido (delta_time en segundos)
void render() override; // Muestra el sprite por pantalla
// --- Gestión de recorridos ---
void addPath(Path path, bool centered = false); // Añade un recorrido (Path)
void addPath(const std::vector<SDL_FPoint>& spots, float waiting_time_s = 0.0F); // Añade un recorrido a partir de puntos
void addPath(float start, float end, PathType type, float fixed_pos, float duration_s, const std::function<double(double)>& easing_function, float waiting_time_s = 0.0F); // Añade un recorrido generado
// --- Estado y control ---
void enable(); // Habilita el objeto
[[nodiscard]] auto hasFinished() const -> bool; // Indica si ha terminado todos los recorridos
// --- Getters ---
[[nodiscard]] auto getCurrentPath() const -> int { return current_path_; } // Devuelve el índice del recorrido actual
private:
// --- Variables internas ---
bool enabled_ = false; // Indica si el objeto está habilitado
bool has_finished_ = false; // Indica si el objeto ha finalizado el recorrido
int current_path_ = 0; // Recorrido que se está recorriendo actualmente
std::vector<Path> paths_; // Caminos a recorrer por el sprite
// --- Métodos internos ---
void moveThroughCurrentPath(float delta_time); // Coloca el sprite en los diferentes puntos del recorrido
void goToNextPathOrDie(); // Cambia de recorrido o finaliza
// --- Métodos auxiliares para addPath ---
[[nodiscard]] static auto determineCenteringType(const Path& path, bool centered) -> PathCentered; // Determina el tipo de centrado
static void centerPathOnX(Path& path, float offset); // Aplica centrado en el eje X
static void centerPathOnY(Path& path, float offset); // Aplica centrado en el eje Y
};

View File

@@ -0,0 +1,89 @@
#include "smart_sprite.hpp"
#include "moving_sprite.hpp" // Para MovingSprite
// Actualiza la posición y comprueba si ha llegado a su destino (time-based)
void SmartSprite::update(float delta_time) {
if (enabled_) {
MovingSprite::update(delta_time);
checkMove();
checkFinished(delta_time);
}
}
// Dibuja el sprite
void SmartSprite::render() {
if (enabled_) {
MovingSprite::render();
}
}
// Comprueba el movimiento
void SmartSprite::checkMove() {
// Comprueba si se desplaza en el eje X hacia la derecha
if (getAccelX() > 0 || getVelX() > 0) {
// Comprueba si ha llegado al destino
if (getPosX() > dest_x_) {
// Lo coloca en posición
setPosX(dest_x_);
// Lo detiene
setVelX(0.0F);
setAccelX(0.0F);
}
}
// Comprueba si se desplaza en el eje X hacia la izquierda
else if (getAccelX() < 0 || getVelX() < 0) {
// Comprueba si ha llegado al destino
if (getPosX() < dest_x_) {
// Lo coloca en posición
setPosX(dest_x_);
// Lo detiene
setVelX(0.0F);
setAccelX(0.0F);
}
}
// Comprueba si se desplaza en el eje Y hacia abajo
if (getAccelY() > 0 || getVelY() > 0) {
// Comprueba si ha llegado al destino
if (getPosY() > dest_y_) {
// Lo coloca en posición
setPosY(dest_y_);
// Lo detiene
setVelY(0.0F);
setAccelY(0.0F);
}
}
// Comprueba si se desplaza en el eje Y hacia arriba
else if (getAccelY() < 0 || getVelY() < 0) {
// Comprueba si ha llegado al destino
if (getPosY() < dest_y_) {
// Lo coloca en posición
setPosY(dest_y_);
// Lo detiene
setVelY(0.0F);
setAccelY(0.0F);
}
}
}
// Comprueba si ha terminado (time-based)
void SmartSprite::checkFinished(float delta_time) {
// Comprueba si ha llegado a su destino
on_destination_ = (getPosX() == dest_x_ && getPosY() == dest_y_);
if (on_destination_) {
if (finished_delay_ms_ == 0.0F) {
finished_ = true;
} else {
finished_timer_ += delta_time;
if (finished_timer_ >= finished_delay_ms_) {
finished_ = true;
}
}
}
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <memory> // Para shared_ptr
#include <utility>
#include "animated_sprite.hpp" // Para AnimatedSprite
class Texture;
// --- Clase SmartSprite: sprite animado que se mueve hacia un destino y puede deshabilitarse automáticamente ---
class SmartSprite : public AnimatedSprite {
public:
// --- Constructor y destructor ---
explicit SmartSprite(std::shared_ptr<Texture> texture)
: AnimatedSprite(std::move(texture)) {}
~SmartSprite() override = default;
// --- Métodos principales ---
void update(float delta_time) override; // Actualiza la posición y comprueba si ha llegado a su destino (time-based)
void render() override; // Dibuja el sprite
// --- Getters ---
[[nodiscard]] auto getDestX() const -> int { return dest_x_; } // Obtiene la posición de destino en X
[[nodiscard]] auto getDestY() const -> int { return dest_y_; } // Obtiene la posición de destino en Y
[[nodiscard]] auto isOnDestination() const -> bool { return on_destination_; } // Indica si está en el destino
[[nodiscard]] auto hasFinished() const -> bool { return finished_; } // Indica si ya ha terminado
// --- Setters ---
void setFinishedDelay(float value) { finished_delay_ms_ = value; } // Establece el retraso para deshabilitarlo (ms)
void setDestX(int x) { dest_x_ = x; } // Establece la posición de destino en X
void setDestY(int y) { dest_y_ = y; } // Establece la posición de destino en Y
void setEnabled(bool value) { enabled_ = value; } // Habilita o deshabilita el objeto
private:
// --- Variables de estado ---
int dest_x_ = 0; // Posición de destino en el eje X
int dest_y_ = 0; // Posición de destino en el eje Y
float finished_delay_ms_ = 0.0F; // Retraso para deshabilitarlo (ms)
float finished_timer_ = 0.0F; // Timer acumulado (ms)
bool on_destination_ = false; // Indica si está en el destino
bool finished_ = false; // Indica si ya ha terminado
bool enabled_ = false; // Indica si el objeto está habilitado
// --- Métodos internos ---
void checkFinished(float delta_time); // Comprueba si ha terminado (time-based)
void checkMove(); // Comprueba el movimiento
};

View File

@@ -0,0 +1,54 @@
#include "sprite.hpp"
#include <utility>
#include <vector> // Para vector
#include "texture.hpp" // Para Texture
// Constructor
Sprite::Sprite(std::shared_ptr<Texture> texture, float pos_x, float pos_y, float width, float height)
: textures_{std::move(texture)},
pos_((SDL_FRect){.x = pos_x, .y = pos_y, .w = width, .h = height}),
sprite_clip_((SDL_FRect){.x = 0, .y = 0, .w = pos_.w, .h = pos_.h}) {}
Sprite::Sprite(std::shared_ptr<Texture> texture, SDL_FRect rect)
: textures_{std::move(texture)},
pos_(rect),
sprite_clip_((SDL_FRect){.x = 0, .y = 0, .w = pos_.w, .h = pos_.h}) {}
Sprite::Sprite(std::shared_ptr<Texture> texture)
: textures_{std::move(texture)},
pos_(SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(textures_.at(texture_index_)->getWidth()), .h = static_cast<float>(textures_.at(texture_index_)->getHeight())}),
sprite_clip_(pos_) {}
// Muestra el sprite por pantalla
void Sprite::render() {
textures_.at(texture_index_)->render(pos_.x, pos_.y, &sprite_clip_, zoom_, zoom_);
}
// Establece la posición del objeto
void Sprite::setPosition(float pos_x, float pos_y) {
pos_.x = pos_x;
pos_.y = pos_y;
}
// Establece la posición del objeto
void Sprite::setPosition(SDL_FPoint point) {
pos_.x = point.x;
pos_.y = point.y;
}
// Reinicia las variables a cero
void Sprite::clear() {
pos_ = {.x = 0, .y = 0, .w = 0, .h = 0};
sprite_clip_ = {.x = 0, .y = 0, .w = 0, .h = 0};
}
// Cambia la textura activa por índice
auto Sprite::setActiveTexture(size_t index) -> bool {
if (index < textures_.size()) {
texture_index_ = index;
}
return index < textures_.size();
}

View File

@@ -0,0 +1,72 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect, SDL_FPoint
#include <cstddef> // Para size_t
#include <memory> // Para shared_ptr
#include <utility>
#include <vector> // Para vector
class Texture;
// --- Clase Sprite: representa un objeto gráfico básico con posición, tamaño y textura ---
class Sprite {
public:
// --- Constructores y destructor ---
Sprite(std::shared_ptr<Texture> texture, float pos_x, float pos_y, float width, float height);
Sprite(std::shared_ptr<Texture> texture, SDL_FRect rect);
explicit Sprite(std::shared_ptr<Texture> texture);
virtual ~Sprite() = default;
// --- Renderizado y control ---
virtual void render(); // Muestra el sprite por pantalla
virtual void clear(); // Reinicia las variables a cero
// --- Getters de posición y tamaño ---
[[nodiscard]] auto getX() const -> float { return pos_.x; }
[[nodiscard]] auto getY() const -> float { return pos_.y; }
[[nodiscard]] auto getWidth() const -> float { return pos_.w; }
[[nodiscard]] auto getHeight() const -> float { return pos_.h; }
[[nodiscard]] auto getPosition() const -> SDL_FRect { return pos_; }
auto getRect() -> SDL_FRect& { return pos_; }
// --- Setters de posición y tamaño ---
void setX(float pos_x) { pos_.x = pos_x; }
void setY(float pos_y) { pos_.y = pos_y; }
void setWidth(float width) { pos_.w = width; }
void setHeight(float height) { pos_.h = height; }
void setPosition(float pos_x, float pos_y);
void setPosition(SDL_FPoint point);
void setPosition(SDL_FRect rect) { pos_ = rect; }
// --- Zoom ---
void setZoom(float zoom) { zoom_ = zoom; }
// --- Modificación de posición ---
void incX(float value) { pos_.x += value; }
void incY(float value) { pos_.y += value; }
// --- Sprite clip ---
[[nodiscard]] auto getSpriteClip() const -> SDL_FRect { return sprite_clip_; }
void setSpriteClip(SDL_FRect rect) { sprite_clip_ = rect; }
void setSpriteClip(float pos_x, float pos_y, float width, float height) { sprite_clip_ = SDL_FRect{.x = pos_x, .y = pos_y, .w = width, .h = height}; }
// --- Textura ---
[[nodiscard]] auto getTexture() const -> std::shared_ptr<Texture> { return textures_.at(texture_index_); }
void setTexture(std::shared_ptr<Texture> texture) { textures_.at(texture_index_) = std::move(texture); }
void addTexture(const std::shared_ptr<Texture>& texture) { textures_.push_back(texture); }
auto setActiveTexture(size_t index) -> bool; // Cambia la textura activa por índice
[[nodiscard]] auto getActiveTexture() const -> size_t { return texture_index_; } // Alias para getActiveTextureIndex
[[nodiscard]] auto getTextureCount() const -> size_t { return textures_.size(); } // Obtiene el número total de texturas
protected:
// --- Métodos internos ---
auto getTextureRef() -> std::shared_ptr<Texture>& { return textures_.at(texture_index_); } // Obtiene referencia a la textura activa
// --- Variables internas ---
std::vector<std::shared_ptr<Texture>> textures_; // Lista de texturas
size_t texture_index_ = 0; // Índice de la textura activa
SDL_FRect pos_; // Posición y tamaño donde dibujar el sprite
SDL_FRect sprite_clip_; // Rectángulo de origen de la textura que se dibujará en pantalla
double zoom_ = 1.0F; // Zoom aplicado a la textura
};

View File

@@ -0,0 +1,453 @@
#include "text.hpp"
#include <SDL3/SDL.h> // Para SDL_FRect, Uint8, SDL_GetRenderTarget, SDL_RenderClear, SDL_SetRenderDrawColor, SDL_SetRenderTarget, SDL_BLENDMODE_BLEND, SDL_PixelFormat, SDL_TextureAccess, SDL_GetTextureAlphaMod
#include <fstream> // Para basic_ifstream, basic_istream, basic_ostream, operator<<, istream, ifstream, istringstream
#include <iostream> // Para cerr
#include <sstream> // Para basic_istringstream
#include <stdexcept> // Para runtime_error
#include <string_view> // Para string_view
#include <utility> // Para std::cmp_less_equal
#include <vector> // Para vector
#include "color.hpp" // Para Color
#include "resource_helper.hpp" // Para loadFile
#include "screen.hpp" // Para Screen
#include "sprite.hpp" // Para Sprite
#include "texture.hpp" // Para Texture
#include "utils.hpp" // Para getFileName
// Constructor
Text::Text(const std::shared_ptr<Texture>& texture, const std::string& text_file) {
// Carga los offsets desde el fichero
auto tf = loadFile(text_file);
// Inicializa variables desde la estructura
box_height_ = tf->box_height;
box_width_ = tf->box_width;
for (int i = 0; i < 128; ++i) {
offset_[i].x = tf->offset[i].x;
offset_[i].y = tf->offset[i].y;
offset_[i].w = tf->offset[i].w;
}
// Crea los objetos
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
// Inicializa variables
fixed_width_ = false;
}
// Constructor
Text::Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Text::File>& text_file) {
// Inicializa variables desde la estructura
box_height_ = text_file->box_height;
box_width_ = text_file->box_width;
for (int i = 0; i < 128; ++i) {
offset_[i].x = text_file->offset[i].x;
offset_[i].y = text_file->offset[i].y;
offset_[i].w = text_file->offset[i].w;
}
// Crea los objetos
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
// Inicializa variables
fixed_width_ = false;
}
// Constructor con textura blanca opcional
Text::Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Texture>& white_texture, const std::string& text_file) {
// Carga los offsets desde el fichero
auto tf = loadFile(text_file);
// Inicializa variables desde la estructura
box_height_ = tf->box_height;
box_width_ = tf->box_width;
for (int i = 0; i < 128; ++i) {
offset_[i].x = tf->offset[i].x;
offset_[i].y = tf->offset[i].y;
offset_[i].w = tf->offset[i].w;
}
// Crea los objetos
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
white_sprite_ = std::make_unique<Sprite>(white_texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
// Inicializa variables
fixed_width_ = false;
}
// Constructor con textura blanca opcional
Text::Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Texture>& white_texture, const std::shared_ptr<Text::File>& text_file) {
// Inicializa variables desde la estructura
box_height_ = text_file->box_height;
box_width_ = text_file->box_width;
for (int i = 0; i < 128; ++i) {
offset_[i].x = text_file->offset[i].x;
offset_[i].y = text_file->offset[i].y;
offset_[i].w = text_file->offset[i].w;
}
// Crea los objetos
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
white_sprite_ = std::make_unique<Sprite>(white_texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
// Inicializa variables
fixed_width_ = false;
}
// Escribe texto en pantalla
void Text::write(int x, int y, const std::string& text, int kerning, int length) {
int shift = 0;
const std::string_view VISIBLE_TEXT = (length == -1) ? std::string_view(text) : std::string_view(text).substr(0, length);
sprite_->setY(y);
for (const auto CH : VISIBLE_TEXT) {
const auto INDEX = static_cast<unsigned char>(CH);
if (INDEX < offset_.size()) {
sprite_->setSpriteClip(offset_[INDEX].x, offset_[INDEX].y, box_width_, box_height_);
sprite_->setX(x + shift);
sprite_->render();
shift += offset_[INDEX].w + kerning;
}
}
}
// Escribe el texto al doble de tamaño
void Text::write2X(int x, int y, const std::string& text, int kerning, int length) {
int shift = 0;
const std::string_view VISIBLE_TEXT = (length == -1) ? std::string_view(text) : std::string_view(text).substr(0, length);
for (const auto CH : VISIBLE_TEXT) {
const auto INDEX = static_cast<unsigned char>(CH);
if (INDEX < offset_.size()) {
SDL_FRect rect = {
.x = static_cast<float>(offset_[INDEX].x),
.y = static_cast<float>(offset_[INDEX].y),
.w = static_cast<float>(box_width_),
.h = static_cast<float>(box_height_)};
sprite_->getTexture()->render(x + shift, y, &rect, 2.0F, 2.0F);
shift += (offset_[INDEX].w + kerning) * 2;
}
}
}
// Escribe el texto en una textura
auto Text::writeToTexture(const std::string& text, int zoom, int kerning, int length) -> std::shared_ptr<Texture> {
auto* renderer = Screen::get()->getRenderer();
auto texture = std::make_shared<Texture>(renderer);
auto width = Text::length(text, kerning) * zoom;
auto height = box_height_ * zoom;
auto* temp = SDL_GetRenderTarget(renderer);
texture->createBlank(width, height, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET);
texture->setBlendMode(SDL_BLENDMODE_BLEND);
texture->setAsRenderTarget(renderer);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
zoom == 1 ? write(0, 0, text, kerning) : write2X(0, 0, text, kerning);
SDL_SetRenderTarget(renderer, temp);
return texture;
}
// Escribe el texto con extras en una textura
auto Text::writeDXToTexture(Uint8 flags, const std::string& text, int kerning, Color text_color, Uint8 shadow_distance, Color shadow_color, int length) -> std::shared_ptr<Texture> {
auto* renderer = Screen::get()->getRenderer();
auto texture = std::make_shared<Texture>(renderer);
// Calcula las dimensiones considerando los efectos
auto base_width = Text::length(text, kerning);
auto base_height = box_height_;
auto width = base_width;
auto height = base_height;
auto offset_x = 0;
auto offset_y = 0;
const auto STROKED = ((flags & Text::STROKE) == Text::STROKE);
const auto SHADOWED = ((flags & Text::SHADOW) == Text::SHADOW);
if (STROKED) {
// Para stroke, el texto se expande en todas las direcciones por shadow_distance
width = base_width + (shadow_distance * 2);
height = base_height + (shadow_distance * 2);
offset_x = shadow_distance;
offset_y = shadow_distance;
} else if (SHADOWED) {
// Para shadow, solo se añade espacio a la derecha y abajo
width = base_width + shadow_distance;
height = base_height + shadow_distance;
}
auto* temp = SDL_GetRenderTarget(renderer);
texture->createBlank(width, height, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET);
texture->setBlendMode(SDL_BLENDMODE_BLEND);
texture->setAsRenderTarget(renderer);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
writeDX(flags, offset_x, offset_y, text, kerning, text_color, shadow_distance, shadow_color, length);
SDL_SetRenderTarget(renderer, temp);
return texture;
}
// Escribe el texto con colores
void Text::writeColored(int x, int y, const std::string& text, Color color, int kerning, int length) {
writeColoredWithSprite(sprite_.get(), x, y, text, color, kerning, length);
}
// Escribe el texto con colores usando un sprite específico
void Text::writeColoredWithSprite(Sprite* sprite, int x, int y, const std::string& text, Color color, int kerning, int length) {
int shift = 0;
const std::string_view VISIBLE_TEXT = (length == -1) ? std::string_view(text) : std::string_view(text).substr(0, length);
auto* texture = sprite->getTexture().get();
// Guarda el alpha original y aplica el nuevo
Uint8 original_alpha;
SDL_GetTextureAlphaMod(texture->getSDLTexture(), &original_alpha);
texture->setAlpha(color.a);
texture->setColor(color.r, color.g, color.b);
sprite->setY(y);
for (const auto CH : VISIBLE_TEXT) {
const auto INDEX = static_cast<unsigned char>(CH);
if (INDEX < offset_.size()) {
sprite->setSpriteClip(offset_[INDEX].x, offset_[INDEX].y, box_width_, box_height_);
sprite->setX(x + shift);
sprite->render();
shift += offset_[INDEX].w + kerning;
}
}
// Restaura los valores originales
texture->setColor(255, 255, 255);
texture->setAlpha(255);
}
// Escribe stroke con alpha correcto usando textura temporal
void Text::writeStrokeWithAlpha(int x, int y, const std::string& text, int kerning, Color stroke_color, Uint8 shadow_distance, int length) {
auto* renderer = Screen::get()->getRenderer();
auto* original_target = SDL_GetRenderTarget(renderer);
// Calcula dimensiones de la textura temporal
auto text_width = Text::length(text, kerning);
auto text_height = box_height_;
auto temp_width = text_width + (shadow_distance * 2);
auto temp_height = text_height + (shadow_distance * 2);
// Crea textura temporal
auto temp_texture = std::make_shared<Texture>(renderer);
temp_texture->createBlank(temp_width, temp_height, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET);
temp_texture->setBlendMode(SDL_BLENDMODE_BLEND);
// Renderiza el stroke en la textura temporal
temp_texture->setAsRenderTarget(renderer);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
// Selecciona el sprite apropiado para el stroke
auto* stroke_sprite = white_sprite_ ? white_sprite_.get() : sprite_.get();
// Renderiza stroke sin alpha (sólido) en textura temporal
Color solid_color = Color(stroke_color.r, stroke_color.g, stroke_color.b, 255);
for (int dist = 1; std::cmp_less_equal(dist, shadow_distance); ++dist) {
for (int dy = -dist; dy <= dist; ++dy) {
for (int dx = -dist; dx <= dist; ++dx) {
writeColoredWithSprite(stroke_sprite, shadow_distance + dx, shadow_distance + dy, text, solid_color, kerning, length);
}
}
}
// Restaura render target original
SDL_SetRenderTarget(renderer, original_target);
// Renderiza la textura temporal con el alpha deseado
temp_texture->setAlpha(stroke_color.a);
temp_texture->render(x - shadow_distance, y - shadow_distance);
}
// Escribe el texto con sombra
void Text::writeShadowed(int x, int y, const std::string& text, Color color, Uint8 shadow_distance, int kerning, int length) {
writeDX(Text::SHADOW, x, y, text, kerning, color, shadow_distance, color, length);
write(x, y, text, kerning, length); // Dibuja el texto principal encima
}
// Escribe el texto centrado en un punto x
void Text::writeCentered(int x, int y, const std::string& text, int kerning, int length) {
x -= (Text::length(text, kerning) / 2);
write(x, y, text, kerning, length);
}
// Renderiza sombra del texto
void Text::renderShadow(int x, int y, const std::string& text, Color shadow_color, int kerning, int length, Uint8 shadow_distance) {
if (white_sprite_) {
writeColoredWithSprite(white_sprite_.get(), x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, length);
} else {
writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, length);
}
}
// Renderiza stroke sólido (método tradicional para stroke sin alpha)
void Text::renderSolidStroke(int x, int y, const std::string& text, Color stroke_color, int kerning, int length, Uint8 shadow_distance) {
for (int dist = 1; std::cmp_less_equal(dist, shadow_distance); ++dist) {
for (int dy = -dist; dy <= dist; ++dy) {
for (int dx = -dist; dx <= dist; ++dx) {
if (white_sprite_) {
writeColoredWithSprite(white_sprite_.get(), x + dx, y + dy, text, stroke_color, kerning, length);
} else {
writeColored(x + dx, y + dy, text, stroke_color, kerning, length);
}
}
}
}
}
// Escribe texto con extras
void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning, Color text_color, Uint8 shadow_distance, Color shadow_color, int length) {
const auto CENTERED = ((flags & Text::CENTER) == Text::CENTER);
const auto SHADOWED = ((flags & Text::SHADOW) == Text::SHADOW);
const auto COLORED = ((flags & Text::COLOR) == Text::COLOR);
const auto STROKED = ((flags & Text::STROKE) == Text::STROKE);
if (CENTERED) {
x -= (Text::length(text, kerning) / 2);
}
if (SHADOWED) {
renderShadow(x, y, text, shadow_color, kerning, length, shadow_distance);
}
if (STROKED) {
if (shadow_color.a < 255) {
// Usa textura temporal para alpha correcto
writeStrokeWithAlpha(x, y, text, kerning, shadow_color, shadow_distance, length);
} else {
// Método tradicional para stroke sólido
renderSolidStroke(x, y, text, shadow_color, kerning, length, shadow_distance);
}
}
if (COLORED) {
writeColored(x, y, text, text_color, kerning, length);
} else {
write(x, y, text, kerning, length);
}
}
// Escribe texto a partir de un TextStyle
void Text::writeStyle(int x, int y, const std::string& text, const Style& style, int length) {
writeDX(style.flags, x, y, text, style.kerning, style.text_color, style.shadow_distance, style.shadow_color);
}
// Obtiene la longitud en pixels de una cadena
auto Text::length(const std::string& text, int kerning) const -> int {
int shift = 0;
for (const auto& ch : text) {
// Convertimos a unsigned char para obtener el valor ASCII correcto (0-255)
const auto INDEX = static_cast<unsigned char>(ch);
// Verificamos si el carácter está dentro de los límites del array
if (INDEX < offset_.size()) {
shift += (offset_[INDEX].w + kerning);
}
}
// Descuenta el kerning del último caracter si el texto no está vacío
return text.empty() ? 0 : shift - kerning;
}
// Devuelve el valor de la variable
auto Text::getCharacterSize() const -> int {
return box_width_;
}
// Establece si se usa un tamaño fijo de letra
void Text::setFixedWidth(bool value) {
fixed_width_ = value;
}
// Llena una estructuta TextFile desde un fichero
auto Text::loadFile(const std::string& file_path) -> std::shared_ptr<Text::File> {
auto tf = std::make_shared<Text::File>();
// Inicializa a cero el vector con las coordenadas
for (auto& i : tf->offset) {
i.x = 0;
i.y = 0;
i.w = 0;
tf->box_width = 0;
tf->box_height = 0;
}
// Intenta cargar desde ResourceHelper primero
auto resource_data = ResourceHelper::loadFile(file_path);
std::istringstream stream;
bool using_resource_data = false;
if (!resource_data.empty()) {
std::string content(resource_data.begin(), resource_data.end());
stream.str(content);
using_resource_data = true;
}
// Fallback a archivo directo
std::ifstream file;
if (!using_resource_data) {
file.open(file_path);
}
std::istream& input_stream = using_resource_data ? stream : static_cast<std::istream&>(file);
if ((using_resource_data && stream.good()) || (!using_resource_data && file.is_open() && file.good())) {
std::string buffer;
// Lee los dos primeros valores del fichero
std::getline(input_stream, buffer);
std::getline(input_stream, buffer);
tf->box_width = std::stoi(buffer);
std::getline(input_stream, buffer);
std::getline(input_stream, buffer);
tf->box_height = std::stoi(buffer);
// lee el resto de datos del fichero
auto index = 32;
auto line_read = 0;
while (std::getline(input_stream, buffer)) {
// Almacena solo las lineas impares
if (line_read % 2 == 1) {
tf->offset[index++].w = std::stoi(buffer);
}
// Limpia el buffer
buffer.clear();
line_read++;
};
// Cierra el fichero si se usó
if (!using_resource_data && file.is_open()) {
file.close();
}
}
// El fichero no se puede abrir
else {
std::cerr << "Error: Fichero no encontrado " << getFileName(file_path) << '\n';
throw std::runtime_error("Fichero no encontrado: " + getFileName(file_path));
}
// Establece las coordenadas para cada caracter ascii de la cadena y su ancho
for (int i = 32; i < 128; ++i) {
tf->offset[i].x = ((i - 32) % 15) * tf->box_width;
tf->offset[i].y = ((i - 32) / 15) * tf->box_height;
}
return tf;
}

View File

@@ -0,0 +1,102 @@
#pragma once
#include <SDL3/SDL.h> // Para Uint8
#include <array> // Para array
#include <memory> // Para shared_ptr, unique_ptr
#include <string> // Para string
#include "color.hpp" // Para Color
#include "sprite.hpp" // Para Sprite
class Texture;
// --- Clase Text: pinta texto en pantalla a partir de un bitmap ---
class Text {
public:
// --- Constantes para flags de texto ---
static constexpr int COLOR = 1;
static constexpr int SHADOW = 2;
static constexpr int CENTER = 4;
static constexpr int STROKE = 8;
// --- Estructuras ---
struct Offset {
int x, y, w;
};
struct File {
int box_width; // Anchura de la caja de cada caracter en el png
int box_height; // Altura de la caja de cada caracter en el png
std::array<Offset, 128> offset = {}; // Vector con las posiciones y ancho de cada letra
};
struct Style {
Uint8 flags;
Color text_color;
Color shadow_color;
Uint8 shadow_distance;
int kerning;
// Constructor con argumentos por defecto
Style(Uint8 flags = 0,
Color text = Color(),
Color shadow = Color(),
Uint8 distance = 1,
int kern = 1)
: flags(flags),
text_color(text),
shadow_color(shadow),
shadow_distance(distance),
kerning(kern) {}
};
// --- Constructores y destructor ---
Text(const std::shared_ptr<Texture>& texture, const std::string& text_file);
Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Text::File>& text_file);
Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Texture>& white_texture, const std::string& text_file);
Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Texture>& white_texture, const std::shared_ptr<Text::File>& text_file);
~Text() = default;
// --- Métodos de escritura en pantalla ---
void write(int x, int y, const std::string& text, int kerning = 1, int length = -1); // Escribe el texto en pantalla
void write2X(int x, int y, const std::string& text, int kerning = 1, int length = -1); // Escribe el texto al doble de tamaño
// --- Escritura en textura ---
auto writeToTexture(const std::string& text, int zoom = 1, int kerning = 1, int length = -1) -> std::shared_ptr<Texture>; // Escribe el texto en una textura
auto writeDXToTexture(Uint8 flags, const std::string& text, int kerning = 1, Color text_color = Color(), Uint8 shadow_distance = 1, Color shadow_color = Color(), int length = -1) -> std::shared_ptr<Texture>; // Escribe el texto con extras en una textura
// --- Métodos de escritura avanzada ---
void writeColored(int x, int y, const std::string& text, Color color, int kerning = 1, int length = -1); // Escribe el texto con colores
void writeShadowed(int x, int y, const std::string& text, Color color, Uint8 shadow_distance = 1, int kerning = 1, int length = -1); // Escribe el texto con sombra
void writeCentered(int x, int y, const std::string& text, int kerning = 1, int length = -1); // Escribe el texto centrado en un punto x
void writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning = 1, Color text_color = Color(), Uint8 shadow_distance = 1, Color shadow_color = Color(), int length = -1); // Escribe texto con extras
void writeStyle(int x, int y, const std::string& text, const Style& style, int length = -1); // Escribe texto a partir de un TextStyle
// --- Utilidades ---
[[nodiscard]] auto length(const std::string& text, int kerning = 1) const -> int; // Obtiene la longitud en pixels de una cadena
[[nodiscard]] auto getCharacterSize() const -> int; // Devuelve el tamaño de caracter actual
// --- Configuración ---
void setFixedWidth(bool value); // Establece si se usa un tamaño fijo de letra
// --- Métodos estáticos ---
static auto loadFile(const std::string& file_path) -> std::shared_ptr<Text::File>; // Llena una estructura Text::File desde un fichero
// --- Métodos privados ---
void writeColoredWithSprite(Sprite* sprite, int x, int y, const std::string& text, Color color, int kerning = 1, int length = -1); // Escribe con un sprite específico
void writeStrokeWithAlpha(int x, int y, const std::string& text, int kerning, Color stroke_color, Uint8 shadow_distance, int length = -1); // Escribe stroke con alpha correcto
void renderShadow(int x, int y, const std::string& text, Color shadow_color, int kerning, int length, Uint8 shadow_distance); // Renderiza sombra del texto
void renderSolidStroke(int x, int y, const std::string& text, Color stroke_color, int kerning, int length, Uint8 shadow_distance); // Renderiza stroke sólido
private:
// --- Objetos y punteros ---
std::unique_ptr<Sprite> sprite_ = nullptr; // Objeto con los gráficos para el texto
std::unique_ptr<Sprite> white_sprite_ = nullptr; // Objeto con los gráficos en blanco para efectos
// --- Variables de estado ---
std::array<Offset, 128> offset_ = {}; // Vector con las posiciones y ancho de cada letra
int box_width_ = 0; // Anchura de la caja de cada caracter en el png
int box_height_ = 0; // Altura de la caja de cada caracter en el png
bool fixed_width_ = false; // Indica si el texto se ha de escribir con longitud fija
};

View File

@@ -0,0 +1,430 @@
#define STB_IMAGE_IMPLEMENTATION
#include "texture.hpp"
#include <SDL3/SDL.h> // Para SDL_LogError, SDL_LogCategory, Uint8, SDL_...
#include <cstdint> // Para uint32_t
#include <cstring> // Para memcpy
#include <fstream> // Para basic_ifstream, basic_istream, basic_ios
#include <iostream> // Para std::cout
#include <sstream> // Para basic_istringstream
#include <stdexcept> // Para runtime_error
#include <string> // Para basic_string, char_traits, operator+, string
#include <utility>
#include <vector> // Para vector
#include "color.hpp" // Para getFileName, Color
#include "external/gif.hpp" // Para Gif
#include "resource_helper.hpp" // Para ResourceHelper
#include "stb_image.h" // Para stbi_image_free, stbi_load, STBI_rgb_alpha
#include "utils.hpp"
// Constructor
Texture::Texture(SDL_Renderer* renderer, std::string path)
: renderer_(renderer),
path_(std::move(path)) {
// Carga el fichero en la textura
if (!path_.empty()) {
// Obtiene la extensión
const std::string EXTENSION = path_.substr(path_.find_last_of('.') + 1);
// .png
if (EXTENSION == "png") {
loadFromFile(path_);
}
// .gif
else if (EXTENSION == "gif") {
// Crea la surface desde un fichero
surface_ = loadSurface(path_);
// Añade la propia paleta del fichero a la lista
addPaletteFromGifFile(path_);
// Crea la textura, establece el BlendMode y copia la surface a la textura
createBlank(width_, height_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING);
SDL_SetTextureBlendMode(texture_, SDL_BLENDMODE_BLEND);
flipSurface();
}
}
}
// Destructor
Texture::~Texture() {
unloadTexture();
unloadSurface();
palettes_.clear();
}
// Carga una imagen desde un fichero
auto Texture::loadFromFile(const std::string& file_path) -> bool {
if (file_path.empty()) {
return false;
}
int req_format = STBI_rgb_alpha;
int width;
int height;
int orig_format;
unsigned char* data = nullptr;
// Intentar cargar desde ResourceHelper primero
auto resource_data = ResourceHelper::loadFile(file_path);
if (!resource_data.empty()) {
data = stbi_load_from_memory(resource_data.data(), resource_data.size(), &width, &height, &orig_format, req_format);
}
// Fallback a filesystem directo
if (data == nullptr) {
data = stbi_load(file_path.c_str(), &width, &height, &orig_format, req_format);
}
if (data == nullptr) {
std::cout << "Error: Fichero no encontrado " << getFileName(file_path) << '\n';
throw std::runtime_error("Fichero no encontrado: " + getFileName(file_path));
}
int pitch;
SDL_PixelFormat pixel_format;
pitch = 4 * width;
pixel_format = SDL_PIXELFORMAT_RGBA32;
// Limpia
unloadTexture();
// La textura final
SDL_Texture* new_texture = nullptr;
// Carga la imagen desde una ruta específica
auto* loaded_surface = SDL_CreateSurfaceFrom(width, height, pixel_format, static_cast<void*>(data), pitch);
if (loaded_surface == nullptr) {
std::cout << "Unable to load image " << file_path << '\n';
} else {
// Crea la textura desde los pixels de la surface
new_texture = SDL_CreateTextureFromSurface(renderer_, loaded_surface);
if (new_texture == nullptr) {
std::cout << "Unable to create texture from " << file_path << "! SDL Error: " << SDL_GetError() << '\n';
} else {
// Obtiene las dimensiones de la imagen
width_ = loaded_surface->w;
height_ = loaded_surface->h;
}
// Elimina la textura cargada
SDL_DestroySurface(loaded_surface);
}
// Return success
stbi_image_free(data);
texture_ = new_texture;
return texture_ != nullptr;
}
// Crea una textura en blanco
auto Texture::createBlank(int width, int height, SDL_PixelFormat format, SDL_TextureAccess access) -> bool {
// Crea una textura sin inicializar
texture_ = SDL_CreateTexture(renderer_, format, access, width, height);
if (texture_ == nullptr) {
std::cout << "Unable to create blank texture! SDL Error: " << SDL_GetError() << '\n';
} else {
width_ = width;
height_ = height;
}
return texture_ != nullptr;
}
// Libera la memoria de la textura
void Texture::unloadTexture() {
// Libera la textura
if (texture_ != nullptr) {
SDL_DestroyTexture(texture_);
texture_ = nullptr;
width_ = 0;
height_ = 0;
}
}
// Establece el color para la modulacion
void Texture::setColor(Uint8 red, Uint8 green, Uint8 blue) {
SDL_SetTextureColorMod(texture_, red, green, blue);
}
void Texture::setColor(Color color) {
SDL_SetTextureColorMod(texture_, color.r, color.g, color.b);
}
// Establece el blending
void Texture::setBlendMode(SDL_BlendMode blending) {
SDL_SetTextureBlendMode(texture_, blending);
}
// Establece el alpha para la modulación
void Texture::setAlpha(Uint8 alpha) {
SDL_SetTextureAlphaMod(texture_, alpha);
}
// Renderiza la textura en un punto específico
void Texture::render(int x, int y, SDL_FRect* clip, float horizontal_zoom, float vertical_zoom, double angle, SDL_FPoint* center, SDL_FlipMode flip) {
// Establece el destino de renderizado en la pantalla
SDL_FRect render_quad = {.x = static_cast<float>(x), .y = static_cast<float>(y), .w = static_cast<float>(width_), .h = static_cast<float>(height_)};
// Obtiene las dimesiones del clip de renderizado
if (clip != nullptr) {
render_quad.w = clip->w;
render_quad.h = clip->h;
}
// Calcula el zoom y las coordenadas
if (vertical_zoom != 1.0F || horizontal_zoom != 1.0F) {
render_quad.x = render_quad.x + (render_quad.w / 2);
render_quad.y = render_quad.y + (render_quad.h / 2);
render_quad.w = render_quad.w * horizontal_zoom;
render_quad.h = render_quad.h * vertical_zoom;
render_quad.x = render_quad.x - (render_quad.w / 2);
render_quad.y = render_quad.y - (render_quad.h / 2);
}
// Renderiza a pantalla
SDL_RenderTextureRotated(renderer_, texture_, clip, &render_quad, angle, center, flip);
}
// Establece la textura como objetivo de renderizado
void Texture::setAsRenderTarget(SDL_Renderer* renderer) {
SDL_SetRenderTarget(renderer, texture_);
}
// Obtiene el ancho de la imagen
auto Texture::getWidth() const -> int {
return width_;
}
// Obtiene el alto de la imagen
auto Texture::getHeight() const -> int {
return height_;
}
// Recarga la textura
auto Texture::reLoad() -> bool {
return loadFromFile(path_);
}
// Obtiene la textura
auto Texture::getSDLTexture() -> SDL_Texture* {
return texture_;
}
// Desencadenar la superficie actual
void Texture::unloadSurface() {
surface_.reset(); // Resetea el shared_ptr
width_ = 0;
height_ = 0;
}
// Crea una surface desde un fichero .gif
auto Texture::loadSurface(const std::string& file_path) -> std::shared_ptr<Surface> {
// Libera la superficie actual
unloadSurface();
std::vector<Uint8> buffer;
// Intentar cargar desde ResourceHelper primero
auto resource_data = ResourceHelper::loadFile(file_path);
if (!resource_data.empty()) {
buffer = resource_data;
} else {
// Fallback a filesystem directo
std::ifstream file(file_path, std::ios::binary | std::ios::ate);
if (!file) {
std::cout << "Error: Fichero no encontrado " << file_path << '\n';
throw std::runtime_error("Fichero no encontrado: " + file_path);
}
// Obtener el tamaño del archivo
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
// Leer el contenido del archivo en un buffer
buffer.resize(size);
if (!file.read(reinterpret_cast<char*>(buffer.data()), size)) {
std::cout << "Error al leer el fichero " << file_path << '\n';
throw std::runtime_error("Error al leer el fichero: " + file_path);
}
}
// Crear un objeto Gif y llamar a la función loadGif
GIF::Gif gif;
Uint16 w = 0;
Uint16 h = 0;
std::vector<Uint8> raw_pixels = gif.loadGif(buffer.data(), w, h);
if (raw_pixels.empty()) {
std::cout << "Error: No se pudo cargar el GIF " << file_path << '\n';
return nullptr;
}
// Si el constructor de Surface espera un std::shared_ptr<Uint8[]>:
size_t pixel_count = raw_pixels.size();
auto pixels = std::shared_ptr<Uint8[]>(new Uint8[pixel_count], std::default_delete<Uint8[]>()); // NOLINT(modernize-avoid-c-arrays)
std::memcpy(pixels.get(), raw_pixels.data(), pixel_count);
auto surface = std::make_shared<Surface>(w, h, pixels);
// Actualizar las dimensiones
width_ = w;
height_ = h;
return surface;
}
// Vuelca la surface en la textura
void Texture::flipSurface() {
// Limpia la textura
auto* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, texture_);
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0);
SDL_RenderClear(renderer_);
SDL_SetRenderTarget(renderer_, temp);
// Vuelca los datos
Uint32* pixels;
int pitch;
SDL_LockTexture(texture_, nullptr, reinterpret_cast<void**>(&pixels), &pitch);
for (int i = 0; i < width_ * height_; ++i) {
pixels[i] = palettes_[current_palette_][surface_->data[i]];
}
SDL_UnlockTexture(texture_);
}
// Establece un color de la paleta
void Texture::setPaletteColor(int palette, int index, Uint32 color) {
palettes_.at(palette)[index] = color;
}
// Carga una paleta desde un fichero
auto Texture::loadPaletteFromFile(const std::string& file_path) -> Palette {
Palette palette;
std::vector<Uint8> buffer;
// Intentar cargar desde ResourceHelper primero
auto resource_data = ResourceHelper::loadFile(file_path);
if (!resource_data.empty()) {
buffer = resource_data;
} else {
// Fallback a filesystem directo
std::ifstream file(file_path, std::ios::binary | std::ios::ate);
if (!file) {
std::cout << "Error: Fichero no encontrado " << file_path << '\n';
throw std::runtime_error("Fichero no encontrado: " + file_path);
}
// Obtener el tamaño del archivo y leerlo en un buffer
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
buffer.resize(size);
if (!file.read(reinterpret_cast<char*>(buffer.data()), size)) {
std::cout << "Error: No se pudo leer completamente el fichero " << file_path << '\n';
throw std::runtime_error("Error al leer el fichero: " + file_path);
}
}
// Usar la nueva función loadPalette, que devuelve un vector<uint32_t>
GIF::Gif gif;
std::vector<uint32_t> pal = gif.loadPalette(buffer.data());
if (pal.empty()) {
std::cout << "Advertencia: No se encontró paleta en el archivo " << file_path << '\n';
return palette; // Devuelve un vector vacío si no hay paleta
}
// Modificar la conversión para obtener formato RGBA (0xRRGGBBAA)
for (size_t i = 0; i < pal.size() && i < palette.size(); ++i) {
palette[i] = (pal[i] << 8) | 0xFF; // Resultado: 0xRRGGBBAA
}
return palette;
}
// Añade una paleta a la lista
void Texture::addPaletteFromGifFile(const std::string& path) {
palettes_.emplace_back(loadPaletteFromFile(path));
setPaletteColor(palettes_.size() - 1, 0, 0x00000000);
}
// Añade una paleta a la lista
void Texture::addPaletteFromPalFile(const std::string& path) {
palettes_.emplace_back(readPalFile(path));
setPaletteColor(palettes_.size() - 1, 0, 0x00000000);
}
// Cambia la paleta de la textura
void Texture::setPalette(size_t palette) {
if (palette < palettes_.size()) {
current_palette_ = palette;
flipSurface();
}
}
// Obtiene el renderizador
auto Texture::getRenderer() -> SDL_Renderer* { return renderer_; }
// Carga una paleta desde un archivo .pal
auto Texture::readPalFile(const std::string& file_path) -> Palette {
Palette palette{};
palette.fill(0); // Inicializar todo con 0 (transparente por defecto)
// Intentar cargar desde ResourceHelper primero
auto resource_data = ResourceHelper::loadFile(file_path);
std::istringstream stream;
bool using_resource_data = false;
if (!resource_data.empty()) {
std::string content(resource_data.begin(), resource_data.end());
stream.str(content);
using_resource_data = true;
}
// Fallback a archivo directo
std::ifstream file;
if (!using_resource_data) {
file.open(file_path);
if (!file.is_open()) {
throw std::runtime_error("No se pudo abrir el archivo .pal");
}
}
std::istream& input_stream = using_resource_data ? stream : static_cast<std::istream&>(file);
std::string line;
int line_number = 0;
int color_index = 0;
while (std::getline(input_stream, line)) {
++line_number;
// Ignorar las tres primeras líneas del archivo
if (line_number <= 3) {
continue;
}
// Procesar las líneas restantes con valores RGB
std::istringstream ss(line);
int r;
int g;
int b;
if (ss >> r >> g >> b) {
// Construir el color RGBA (A = 255 por defecto)
Uint32 color = (r << 24) | (g << 16) | (b << 8) | 255;
palette[color_index++] = color;
// Limitar a un máximo de 256 colores (opcional)
if (color_index >= 256) {
break;
}
}
}
if (!using_resource_data && file.is_open()) {
file.close();
}
return palette;
}

View File

@@ -0,0 +1,83 @@
#pragma once
#include <SDL3/SDL.h> // Para Uint8, SDL_Renderer, Uint16, SDL_FlipMode, SDL_PixelFormat, SDL_TextureAccess, SDL_Texture, Uint32, SDL_BlendMode, SDL_FPoint, SDL_FRect
#include <array> // Para array
#include <cstddef> // Para size_t
#include <memory> // Para shared_ptr
#include <string> // Para string, basic_string
#include <utility>
#include <vector> // Para vector
struct Color;
// Alias
using Palette = std::array<Uint32, 256>;
// Definición de Surface para imágenes con paleta
struct Surface {
std::shared_ptr<Uint8[]> data; // NOLINT(modernize-avoid-c-arrays)
Uint16 w, h;
// Constructor
Surface(Uint16 width, Uint16 height, std::shared_ptr<Uint8[]> pixels) // NOLINT(modernize-avoid-c-arrays)
: data(std::move(pixels)),
w(width),
h(height) {}
};
// Clase Texture: gestiona texturas, paletas y renderizado
class Texture {
public:
// --- Constructores y destructor ---
explicit Texture(SDL_Renderer* renderer, std::string path = std::string());
~Texture();
// --- Carga y creación ---
auto loadFromFile(const std::string& path) -> bool; // Carga una imagen desde un fichero
auto createBlank(int width, int height, SDL_PixelFormat format = SDL_PIXELFORMAT_RGBA8888, SDL_TextureAccess access = SDL_TEXTUREACCESS_STREAMING) -> bool; // Crea una textura en blanco
auto reLoad() -> bool; // Recarga la textura
// --- Renderizado ---
void render(int x, int y, SDL_FRect* clip = nullptr, float horizontal_zoom = 1, float vertical_zoom = 1, double angle = 0.0, SDL_FPoint* center = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE); // Renderiza la textura en un punto específico
void setAsRenderTarget(SDL_Renderer* renderer); // Establece la textura como objetivo de renderizado
// --- Modificadores de color y blending ---
void setColor(Uint8 red, Uint8 green, Uint8 blue); // Establece el color para la modulación
void setColor(Color color); // Establece el color para la modulación
void setBlendMode(SDL_BlendMode blending); // Establece el blending
void setAlpha(Uint8 alpha); // Establece el alpha para la modulación
// --- Paletas ---
void addPaletteFromGifFile(const std::string& path); // Añade una paleta a la lista
void addPaletteFromPalFile(const std::string& path); // Añade una paleta a la lista
void setPaletteColor(int palette, int index, Uint32 color); // Establece un color de la paleta
void setPalette(size_t palette); // Cambia la paleta de la textura
// --- Getters ---
[[nodiscard]] auto getWidth() const -> int; // Obtiene el ancho de la imagen
[[nodiscard]] auto getHeight() const -> int; // Obtiene el alto de la imagen
auto getSDLTexture() -> SDL_Texture*; // Obtiene la textura SDL
auto getRenderer() -> SDL_Renderer*; // Obtiene el renderizador
private:
// --- Objetos y punteros ---
SDL_Renderer* renderer_; // Renderizador donde dibujar la textura
SDL_Texture* texture_ = nullptr; // La textura
std::shared_ptr<Surface> surface_ = nullptr; // Surface para usar imágenes en formato gif con paleta
// --- Variables ---
std::string path_; // Ruta de la imagen de la textura
int width_ = 0; // Ancho de la imagen
int height_ = 0; // Alto de la imagen
std::vector<Palette> palettes_; // Vector con las diferentes paletas
int current_palette_ = 0; // Índice de la paleta en uso
// --- Métodos internos ---
auto loadSurface(const std::string& file_path) -> std::shared_ptr<Surface>; // Crea una surface desde un fichero .gif
void flipSurface(); // Vuelca la surface en la textura
static auto loadPaletteFromFile(const std::string& file_path) -> Palette; // Carga una paleta desde un fichero
void unloadTexture(); // Libera la memoria de la textura
void unloadSurface(); // Libera la surface actual
static auto readPalFile(const std::string& file_path) -> Palette; // Carga una paleta desde un archivo .pal
};

View File

@@ -0,0 +1,165 @@
#include "tiled_bg.hpp"
#include <SDL3/SDL.h> // Para SDL_SetRenderTarget, SDL_CreateTexture, SDL_DestroyTexture, SDL_FRect, SDL_GetRenderTarget, SDL_RenderTexture, SDL_PixelFormat, SDL_TextureAccess
#include <algorithm> // Para max
#include <cmath> // Para cos, pow, sin
#include <cstdlib> // Para rand
#include <memory> // Para unique_ptr, make_unique
#include <numbers> // Para pi
#include <string> // Para basic_string
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "sprite.hpp" // Para Sprite
// Constructor
TiledBG::TiledBG(SDL_FRect pos, TiledBGMode mode)
: renderer_(Screen::get()->getRenderer()),
pos_(pos),
mode_(mode == TiledBGMode::RANDOM ? static_cast<TiledBGMode>(rand() % 2) : mode) {
// Crea la textura para el mosaico de fondo
canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, pos_.w * 2, pos_.h * 2);
// Rellena la textura con el contenido
fillTexture();
// Inicializa variables
switch (mode_) {
case TiledBGMode::STATIC:
window_ = {.x = 0, .y = 0, .w = pos_.w, .h = pos_.h};
speed_ = 0.0F;
break;
case TiledBGMode::DIAGONAL:
window_ = {.x = 0, .y = 0, .w = pos_.w, .h = pos_.h};
break;
case TiledBGMode::CIRCLE:
window_ = {.x = 128, .y = 128, .w = pos_.w, .h = pos_.h};
break;
default:
window_ = {.x = 0, .y = 0, .w = pos_.w, .h = pos_.h};
break;
}
}
// Destructor
TiledBG::~TiledBG() {
SDL_DestroyTexture(canvas_);
}
// Rellena la textura con el contenido
void TiledBG::fillTexture() {
// Crea los objetos para pintar en la textura de fondo
auto tile = std::make_unique<Sprite>(Resource::get()->getTexture("title_bg_tile.png"), (SDL_FRect){.x = 0, .y = 0, .w = TILE_WIDTH, .h = TILE_HEIGHT});
// Prepara para dibujar sobre la textura
auto* temp = SDL_GetRenderTarget(renderer_);
SDL_SetRenderTarget(renderer_, canvas_);
// Rellena la textura con el tile
const auto I_MAX = pos_.w * 2 / TILE_WIDTH;
const auto J_MAX = pos_.h * 2 / TILE_HEIGHT;
tile->setSpriteClip(0, 0, TILE_WIDTH, TILE_HEIGHT);
for (int i = 0; i < I_MAX; ++i) {
for (int j = 0; j < J_MAX; ++j) {
tile->setX(i * TILE_WIDTH);
tile->setY(j * TILE_HEIGHT);
tile->render();
}
}
// Vuelve a colocar el renderizador como estaba
SDL_SetRenderTarget(renderer_, temp);
}
// Pinta la clase en pantalla
void TiledBG::render() {
SDL_RenderTexture(renderer_, canvas_, &window_, &pos_);
}
// Actualiza la lógica de la clase (time-based)
void TiledBG::update(float delta_time) {
updateSpeedChange(delta_time);
updateDesp(delta_time);
updateStop(delta_time);
switch (mode_) {
case TiledBGMode::DIAGONAL: {
// El tileado de fondo se desplaza en diagonal
window_.x = static_cast<int>(desp_) % TILE_WIDTH;
window_.y = static_cast<int>(desp_) % TILE_HEIGHT;
break;
}
case TiledBGMode::CIRCLE: {
// El tileado de fondo se desplaza en circulo
const float ANGLE_RAD = (desp_ * std::numbers::pi / 180.0F);
window_.x = 128 + static_cast<int>(std::cos(ANGLE_RAD) * 128);
window_.y = 128 + static_cast<int>(std::sin(-ANGLE_RAD) * 96);
break;
}
default:
break;
}
}
// Detiene el desplazamiento de forma ordenada (time-based)
void TiledBG::updateStop(float delta_time) {
if (stopping_) {
const int UMBRAL = STOP_THRESHOLD_FACTOR * speed_;
// Desacelerar si estamos cerca de completar el ciclo (ventana a punto de regresar a 0)
if (window_.x >= TILE_WIDTH - UMBRAL) {
// Aplicar desaceleración time-based
float frame_rate = 60.0F;
float deceleration_per_ms = std::pow(DECELERATION_FACTOR, frame_rate * delta_time / 1000.0F);
speed_ /= deceleration_per_ms;
// Asegura que no baje demasiado
speed_ = std::max(speed_, MIN_SPEED);
}
// Si estamos en 0, detener
if (window_.x == 0) {
speed_ = 0.0F;
stopping_ = false; // Desactivamos el estado de "stopping"
}
}
}
// Cambia la velocidad gradualmente en X segundos
void TiledBG::changeSpeedTo(float target_speed, float duration_s) {
if (duration_s <= 0.0F) {
// Si la duración es 0 o negativa, cambia inmediatamente
speed_ = target_speed;
changing_speed_ = false;
return;
}
// Configurar el cambio gradual
changing_speed_ = true;
initial_speed_ = speed_;
target_speed_ = target_speed;
change_duration_s_ = duration_s;
change_timer_s_ = 0.0F;
}
// Actualiza el cambio gradual de velocidad (time-based)
void TiledBG::updateSpeedChange(float delta_time) {
if (!changing_speed_) {
return;
}
change_timer_s_ += delta_time;
if (change_timer_s_ >= change_duration_s_) {
// Cambio completado
speed_ = target_speed_;
changing_speed_ = false;
} else {
// Interpolación lineal entre velocidad inicial y objetivo
float progress = change_timer_s_ / change_duration_s_;
speed_ = initial_speed_ + ((target_speed_ - initial_speed_) * progress);
}
}

View File

@@ -0,0 +1,70 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect, SDL_SetTextureColorMod, SDL_Renderer, SDL_Texture
#include "color.hpp" // Para Color
// --- Enums ---
enum class TiledBGMode : int { // Modos de funcionamiento para el tileado de fondo
CIRCLE = 0,
DIAGONAL = 1,
RANDOM = 2,
STATIC = 3,
};
// --- Clase TiledBG: dibuja un tileado de fondo con efectos de movimiento ---
// Esta clase se sirve de una textura "canvas", que rellena con los tiles.
// El rectángulo "window" recorre la textura de diferentes formas para generar el efecto de movimiento.
class TiledBG {
public:
// --- Constructores y destructor ---
TiledBG(SDL_FRect pos, TiledBGMode mode);
~TiledBG();
// --- Métodos principales ---
void render(); // Pinta la clase en pantalla
void update(float delta_time); // Actualiza la lógica de la clase
// --- Configuración ---
void setSpeed(float speed) { speed_ = speed; } // Establece la velocidad
void changeSpeedTo(float target_speed, float duration_s); // Cambia la velocidad gradualmente en X segundos
void stopGracefully() { stopping_ = true; } // Detiene el desplazamiento de forma ordenada
void setColor(Color color) { SDL_SetTextureColorMod(canvas_, color.r, color.g, color.b); } // Cambia el color de la textura
// --- Getters ---
[[nodiscard]] auto isStopped() const -> bool { return speed_ == 0.0F; } // Indica si está parado
[[nodiscard]] auto isChangingSpeed() const -> bool { return changing_speed_; } // Indica si está cambiando velocidad gradualmente
private:
// --- Constantes ---
static constexpr int TILE_WIDTH = 64; // Ancho del tile
static constexpr int TILE_HEIGHT = 64; // Alto del tile
static constexpr float STOP_THRESHOLD_FACTOR = 20.0F; // Factor para umbral de parada
static constexpr float DECELERATION_FACTOR = 1.05F; // Factor de desaceleración
static constexpr float MIN_SPEED = 0.1F; // Velocidad mínima
// --- Objetos y punteros ---
SDL_Renderer* renderer_; // El renderizador de la ventana
SDL_Texture* canvas_; // Textura donde dibujar el fondo formado por tiles
// --- Variables de estado ---
SDL_FRect pos_; // Posición y tamaño del mosaico
SDL_FRect window_; // Ventana visible para la textura de fondo del título
TiledBGMode mode_; // Tipo de movimiento del mosaico
float desp_ = 0.0F; // Desplazamiento aplicado
float speed_ = 1.0F; // Incremento que se añade al desplazamiento a cada bucle
bool stopping_ = false; // Indica si se está deteniendo
// --- Variables para cambio gradual de velocidad ---
bool changing_speed_ = false; // Indica si está cambiando velocidad gradualmente
float initial_speed_ = 0.0F; // Velocidad inicial del cambio
float target_speed_ = 0.0F; // Velocidad objetivo del cambio
float change_duration_s_ = 0.0F; // Duración total del cambio en segundos
float change_timer_s_ = 0.0F; // Tiempo transcurrido del cambio
// --- Métodos internos ---
void fillTexture(); // Rellena la textura con el contenido
void updateDesp(float delta_time) { desp_ += speed_ * delta_time; } // Actualiza el desplazamiento (time-based)
void updateStop(float delta_time); // Detiene el desplazamiento de forma ordenada (time-based)
void updateSpeedChange(float delta_time); // Actualiza el cambio gradual de velocidad (time-based)
};

View File

@@ -0,0 +1,106 @@
#include "writer.hpp"
#include "text.hpp" // Para Text
// Actualiza el objeto (delta_time en ms)
void Writer::update(float delta_time) {
if (enabled_) {
if (!completed_) {
// No completado
writing_timer_ += delta_time;
if (writing_timer_ >= speed_interval_) {
index_++;
writing_timer_ = 0.0F;
}
if (index_ == length_) {
completed_ = true;
}
} else {
// Completado
enabled_timer_ += delta_time;
finished_ = enabled_timer_ >= enabled_timer_target_;
}
}
}
// Actualiza el objeto (delta_time en segundos)
void Writer::updateS(float delta_time) {
// Convierte segundos a milisegundos y usa la lógica normal
update(delta_time * 1000.0F);
}
// Dibuja el objeto en pantalla
void Writer::render() const {
if (enabled_) {
text_->write(pos_x_, pos_y_, caption_, kerning_, index_);
}
}
// Establece el valor de la variable
void Writer::setPosX(int value) {
pos_x_ = value;
}
// Establece el valor de la variable
void Writer::setPosY(int value) {
pos_y_ = value;
}
// Establece el valor de la variable
void Writer::setKerning(int value) {
kerning_ = value;
}
// Establece el valor de la variable
void Writer::setCaption(const std::string& text) {
caption_ = text;
length_ = text.length();
}
// Establece el valor de la variable (frames)
void Writer::setSpeed(int value) {
// Convierte frames a milisegundos (frames * 16.67ms)
constexpr float FRAME_TIME_MS = 16.67F;
speed_interval_ = static_cast<float>(value) * FRAME_TIME_MS;
writing_timer_ = 0.0F;
}
// Establece la velocidad en segundos entre caracteres
void Writer::setSpeedS(float value) {
// Convierte segundos a milisegundos para consistencia interna
speed_interval_ = value * 1000.0F;
writing_timer_ = 0.0F;
}
// Establece el valor de la variable
void Writer::setEnabled(bool value) {
enabled_ = value;
}
// Obtiene el valor de la variable
auto Writer::isEnabled() const -> bool {
return enabled_;
}
// Establece el temporizador para deshabilitar el objeto (en milisegundos)
void Writer::setFinishedTimerMs(float time_ms) {
enabled_timer_target_ = time_ms;
enabled_timer_ = 0.0F;
}
// Establece el temporizador para deshabilitar el objeto (en segundos)
void Writer::setFinishedTimerS(float time_s) {
enabled_timer_target_ = time_s * 1000.0F; // Convertir segundos a milisegundos
enabled_timer_ = 0.0F;
}
// Centra la cadena de texto a un punto X
void Writer::center(int x) {
setPosX(x - (text_->length(caption_, kerning_) / 2));
}
// Obtiene el valor de la variable
auto Writer::hasFinished() const -> bool {
return finished_;
}

View File

@@ -0,0 +1,57 @@
#pragma once
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <utility>
class Text;
// --- Clase Writer: pinta texto en pantalla letra a letra ---
class Writer {
public:
// --- Constructor y destructor ---
explicit Writer(std::shared_ptr<Text> text)
: text_(std::move(text)) {}
~Writer() = default;
// --- Métodos principales ---
void update(float delta_time); // Actualiza el objeto (delta_time en ms)
void updateS(float delta_time); // Actualiza el objeto (delta_time en segundos)
void render() const; // Dibuja el objeto en pantalla
// --- Setters ---
void setPosX(int value); // Establece la posición X
void setPosY(int value); // Establece la posición Y
void setKerning(int value); // Establece el kerning (espaciado entre caracteres)
void setCaption(const std::string& text); // Establece el texto a escribir
void setSpeed(int value); // Establece la velocidad de escritura (frames)
void setSpeedS(float value); // Establece la velocidad de escritura (segundos entre caracteres)
void setEnabled(bool value); // Habilita o deshabilita el objeto
void setFinishedTimerMs(float time_ms); // Establece el temporizador para deshabilitar el objeto (en ms)
void setFinishedTimerS(float time_s); // Establece el temporizador para deshabilitar el objeto (en segundos)
void center(int x); // Centra la cadena de texto a un punto X
// --- Getters ---
[[nodiscard]] auto isEnabled() const -> bool; // Indica si el objeto está habilitado
[[nodiscard]] auto hasFinished() const -> bool; // Indica si ya ha terminado
private:
// --- Objetos y punteros ---
std::shared_ptr<Text> text_; // Objeto encargado de escribir el texto
// --- Variables de estado ---
std::string caption_; // El texto para escribir
int pos_x_ = 0; // Posición en el eje X donde empezar a escribir el texto
int pos_y_ = 0; // Posición en el eje Y donde empezar a escribir el texto
int kerning_ = 0; // Kerning del texto, es decir, espaciado entre caracteres
float speed_interval_ = 0.0F; // Intervalo entre caracteres (ms para compatibilidad)
float writing_timer_ = 0.0F; // Temporizador de escritura para cada caracter
int index_ = 0; // Posición del texto que se está escribiendo
int length_ = 0; // Longitud de la cadena a escribir
float enabled_timer_ = 0.0F; // Temporizador para deshabilitar el objeto
float enabled_timer_target_ = 0.0F; // Tiempo objetivo para deshabilitar el objeto
bool completed_ = false; // Indica si se ha escrito todo el texto
bool enabled_ = false; // Indica si el objeto está habilitado
bool finished_ = false; // Indica si ya ha terminado
};

View File

@@ -0,0 +1,318 @@
#include "asset.hpp"
#include <SDL3/SDL.h> // Para SDL_LogWarn, SDL_LogCategory, SDL_LogError
#include <algorithm> // Para sort
#include <cstddef> // Para size_t
#include <exception> // Para exception
#include <filesystem> // Para exists, path
#include <fstream> // Para basic_ifstream, basic_istream, basic_ostream, operator<<, ifstream, istringstream, endl
#include <iostream> // Para cout
#include <sstream> // Para basic_istringstream
#include <stdexcept> // Para runtime_error
#include "resource_helper.hpp" // Para loadFile
#include "utils.hpp" // Para getFileName
// Singleton
Asset* Asset::instance = nullptr;
void Asset::init(const std::string& executable_path) {
Asset::instance = new Asset(executable_path);
}
void Asset::destroy() {
delete Asset::instance;
}
auto Asset::get() -> Asset* {
return Asset::instance;
}
// Añade un elemento al mapa (función auxiliar)
void Asset::addToMap(const std::string& file_path, Type type, bool required, bool absolute) {
std::string full_path = absolute ? file_path : executable_path_ + file_path;
std::string filename = getFileName(full_path);
// Verificar si ya existe el archivo
if (file_list_.contains(filename)) {
std::cout << "Warning: Asset '" << filename << "' already exists, overwriting" << '\n';
}
file_list_.emplace(filename, Item{std::move(full_path), type, required});
}
// Añade un elemento a la lista
void Asset::add(const std::string& file_path, Type type, bool required, bool absolute) {
addToMap(file_path, type, required, absolute);
}
// Carga recursos desde un archivo de configuración con soporte para variables
void Asset::loadFromFile(const std::string& config_file_path, const std::string& prefix, const std::string& system_folder) {
std::ifstream file(config_file_path);
if (!file.is_open()) {
std::cout << "Error: Cannot open config file: " << config_file_path << '\n';
return;
}
std::string line;
int line_number = 0;
while (std::getline(file, line)) {
++line_number;
// Limpiar espacios en blanco al principio y final
line.erase(0, line.find_first_not_of(" \t\r"));
line.erase(line.find_last_not_of(" \t\r") + 1);
// DEBUG: mostrar línea leída (opcional, puedes comentar esta línea)
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Line %d: '%s'", line_number, line.c_str());
// Ignorar líneas vacías y comentarios
if (line.empty() || line[0] == '#' || line[0] == ';') {
continue;
}
// Dividir la línea por el separador '|'
std::vector<std::string> parts;
std::istringstream iss(line);
std::string part;
while (std::getline(iss, part, '|')) {
parts.push_back(part);
}
// Verificar que tenemos al menos tipo y ruta
if (parts.size() < 2) {
std::cout << "Warning: Malformed line " << line_number << " in config file (insufficient fields)" << '\n';
continue;
}
try {
const std::string& type_str = parts[0];
std::string path = parts[1];
// Valores por defecto
bool required = true;
bool absolute = false;
// Si hay opciones en el tercer campo, parsearlas
if (parts.size() >= 3) {
parseOptions(parts[2], required, absolute);
}
// Reemplazar variables en la ruta
path = replaceVariables(path, prefix, system_folder);
// Parsear el tipo de asset
Type type = parseAssetType(type_str);
// Añadir al mapa
addToMap(path, type, required, absolute);
} catch (const std::exception& e) {
std::cout << "Error parsing line " << line_number << " in config file: " << e.what() << '\n';
}
}
file.close();
}
// Devuelve la ruta completa a un fichero (búsqueda O(1))
auto Asset::getPath(const std::string& filename) const -> std::string {
auto it = file_list_.find(filename);
if (it != file_list_.end()) {
return it->second.file;
}
std::cout << "Warning: file " << filename << " not found" << '\n';
return "";
}
// Carga datos del archivo usando ResourceHelper
auto Asset::loadData(const std::string& filename) const -> std::vector<uint8_t> {
auto it = file_list_.find(filename);
if (it != file_list_.end()) {
return ResourceHelper::loadFile(it->second.file);
}
std::cout << "Warning: file " << filename << " not found for data loading" << '\n';
return {};
}
// Verifica si un recurso existe
auto Asset::exists(const std::string& filename) const -> bool {
return file_list_.contains(filename);
}
// Comprueba que existen todos los elementos
auto Asset::check() const -> bool {
bool success = true;
// Agrupar por tipo para mostrar organizado
std::unordered_map<Type, std::vector<const Item*>> by_type;
for (const auto& [filename, item] : file_list_) {
if (item.required) {
by_type[item.type].push_back(&item);
}
}
// Verificar por tipo
for (int type = 0; type < static_cast<int>(Type::SIZE); ++type) {
Type asset_type = static_cast<Type>(type);
if (by_type.contains(asset_type)) {
for (const auto* item : by_type[asset_type]) {
if (!checkFile(item->file)) {
success = false;
}
}
}
}
return success;
}
// Comprueba que existe un fichero
auto Asset::checkFile(const std::string& path) const -> bool {
// Construir ruta del pack usando executable_path_ (misma lógica que Director::init)
#ifdef MACOS_BUNDLE
std::string pack_path = executable_path_ + "../Resources/resources.pack";
#else
std::string pack_path = executable_path_ + "resources.pack";
#endif
bool pack_exists = std::filesystem::exists(pack_path);
if (pack_exists) {
// MODO PACK: Usar ResourceHelper (igual que la carga real)
auto data = ResourceHelper::loadFile(path);
return !data.empty();
} // MODO FILESYSTEM: Verificación directa (modo desarrollo)
std::ifstream file(path);
bool success = file.good();
file.close();
if (!success) {
std::cout << "Error: Could not open file: " << path << '\n';
}
return success;
}
// Parsea string a Type
auto Asset::parseAssetType(const std::string& type_str) -> Type {
if (type_str == "BITMAP") {
return Type::BITMAP;
}
if (type_str == "MUSIC") {
return Type::MUSIC;
}
if (type_str == "SOUND") {
return Type::SOUND;
}
if (type_str == "FONT") {
return Type::FONT;
}
if (type_str == "LANG") {
return Type::LANG;
}
if (type_str == "DATA") {
return Type::DATA;
}
if (type_str == "DEMODATA") {
return Type::DEMODATA;
}
if (type_str == "ANIMATION") {
return Type::ANIMATION;
}
if (type_str == "PALETTE") {
return Type::PALETTE;
}
throw std::runtime_error("Unknown asset type: " + type_str);
}
// Devuelve el nombre del tipo de recurso
auto Asset::getTypeName(Type type) -> std::string {
switch (type) {
case Type::BITMAP:
return "BITMAP";
case Type::MUSIC:
return "MUSIC";
case Type::SOUND:
return "SOUND";
case Type::FONT:
return "FONT";
case Type::LANG:
return "LANG";
case Type::DATA:
return "DATA";
case Type::DEMODATA:
return "DEMODATA";
case Type::ANIMATION:
return "ANIMATION";
case Type::PALETTE:
return "PALETTE";
default:
return "ERROR";
}
}
// Devuelve la lista de recursos de un tipo
auto Asset::getListByType(Type type) const -> std::vector<std::string> {
std::vector<std::string> list;
for (const auto& [filename, item] : file_list_) {
if (item.type == type) {
list.push_back(item.file);
}
}
// Ordenar alfabéticamente para garantizar orden consistente
std::ranges::sort(list);
return list;
}
// Reemplaza variables en las rutas
auto Asset::replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string {
std::string result = path;
// Reemplazar ${PREFIX}
size_t pos = 0;
while ((pos = result.find("${PREFIX}", pos)) != std::string::npos) {
result.replace(pos, 9, prefix); // 9 = longitud de "${PREFIX}"
pos += prefix.length();
}
// Reemplazar ${SYSTEM_FOLDER}
pos = 0;
while ((pos = result.find("${SYSTEM_FOLDER}", pos)) != std::string::npos) {
result.replace(pos, 16, system_folder); // 16 = longitud de "${SYSTEM_FOLDER}"
pos += system_folder.length();
}
return result;
}
// Parsea las opciones de una línea de configuración
auto Asset::parseOptions(const std::string& options, bool& required, bool& absolute) -> void {
if (options.empty()) {
return;
}
std::istringstream iss(options);
std::string option;
while (std::getline(iss, option, ',')) {
// Eliminar espacios
option.erase(0, option.find_first_not_of(" \t"));
option.erase(option.find_last_not_of(" \t") + 1);
if (option == "optional") {
required = false;
} else if (option == "absolute") {
absolute = true;
}
}
}

View File

@@ -0,0 +1,74 @@
#pragma once
#include <cstdint> // Para uint8_t
#include <string> // Para string
#include <unordered_map> // Para unordered_map
#include <utility> // Para move
#include <vector> // Para vector
// --- Clase Asset: gestor optimizado de recursos (singleton) ---
class Asset {
public:
// --- Enums ---
enum class Type : int {
BITMAP, // Imágenes
MUSIC, // Música
SOUND, // Sonidos
FONT, // Fuentes
LANG, // Idiomas
DATA, // Datos
DEMODATA, // Datos de demo
ANIMATION, // Animaciones
PALETTE, // Paletas
SIZE, // Tamaño
};
// --- Métodos de singleton ---
static void init(const std::string& executable_path);
static void destroy();
static auto get() -> Asset*;
Asset(const Asset&) = delete;
auto operator=(const Asset&) -> Asset& = delete;
// --- Métodos para la gestión de recursos ---
void add(const std::string& file_path, Type type, bool required = true, bool absolute = false);
void loadFromFile(const std::string& config_file_path, const std::string& prefix = "", const std::string& system_folder = ""); // Con soporte para variables
[[nodiscard]] auto getPath(const std::string& filename) const -> std::string;
[[nodiscard]] auto loadData(const std::string& filename) const -> std::vector<uint8_t>; // Carga datos del archivo
[[nodiscard]] auto check() const -> bool;
[[nodiscard]] auto getListByType(Type type) const -> std::vector<std::string>;
[[nodiscard]] auto exists(const std::string& filename) const -> bool; // Nueva función para verificar existencia
private:
// --- Estructuras privadas ---
struct Item {
std::string file; // Ruta completa del archivo
Type type; // Tipo de recurso
bool required; // Indica si el archivo es obligatorio
Item(std::string path, Type asset_type, bool is_required)
: file(std::move(path)),
type(asset_type),
required(is_required) {}
};
// --- Variables internas ---
std::unordered_map<std::string, Item> file_list_; // Mapa para búsqueda O(1)
std::string executable_path_; // Ruta del ejecutable
// --- Métodos internos ---
[[nodiscard]] auto checkFile(const std::string& path) const -> bool; // Verifica si un archivo existe
[[nodiscard]] static auto getTypeName(Type type) -> std::string; // Obtiene el nombre del tipo
[[nodiscard]] static auto parseAssetType(const std::string& type_str) -> Type; // Convierte string a tipo
void addToMap(const std::string& file_path, Type type, bool required, bool absolute); // Añade archivo al mapa
[[nodiscard]] static auto replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string; // Reemplaza variables en la ruta
static auto parseOptions(const std::string& options, bool& required, bool& absolute) -> void; // Parsea opciones
// --- Constructores y destructor privados (singleton) ---
explicit Asset(std::string executable_path) // Constructor privado
: executable_path_(std::move(executable_path)) {}
~Asset() = default; // Destructor privado
// --- Instancia singleton ---
static Asset* instance; // Instancia única de Asset
};

View File

@@ -0,0 +1,105 @@
#include "asset_integrated.hpp"
#include <filesystem>
#include <fstream>
#include <iostream>
bool AssetIntegrated::resource_pack_enabled = false;
void AssetIntegrated::initWithResourcePack(const std::string& executable_path,
const std::string& resource_pack_path) {
// Inicializar Asset normal
Asset::init(executable_path);
// Inicializar ResourceLoader
auto& loader = ResourceLoader::getInstance();
resource_pack_enabled = loader.initialize(resource_pack_path, true);
if (resource_pack_enabled) {
std::cout << "Asset system initialized with resource pack: " << resource_pack_path << '\n';
} else {
std::cout << "Asset system initialized in fallback mode (filesystem)" << '\n';
}
}
auto AssetIntegrated::loadFile(const std::string& filename) -> std::vector<uint8_t> {
if (shouldUseResourcePack(filename) && resource_pack_enabled) {
// Intentar cargar del pack de recursos
auto& loader = ResourceLoader::getInstance();
// Convertir ruta completa a ruta relativa para el pack
std::string relative_path = filename;
// Si la ruta contiene "data/", extraer la parte relativa
size_t data_pos = filename.find("data/");
if (data_pos != std::string::npos) {
relative_path = filename.substr(data_pos + 5); // +5 para saltar "data/"
}
auto data = loader.loadResource(relative_path);
if (!data.empty()) {
return data;
}
}
// Fallback: cargar del filesystem
std::ifstream file(filename, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Error: Could not open file: " << filename << '\n';
return {};
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> data(file_size);
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
std::cerr << "Error: Could not read file: " << filename << '\n';
return {};
}
return data;
}
auto AssetIntegrated::fileExists(const std::string& filename) -> bool {
if (shouldUseResourcePack(filename) && resource_pack_enabled) {
auto& loader = ResourceLoader::getInstance();
// Convertir ruta completa a ruta relativa para el pack
std::string relative_path = filename;
size_t data_pos = filename.find("data/");
if (data_pos != std::string::npos) {
relative_path = filename.substr(data_pos + 5);
}
if (loader.resourceExists(relative_path)) {
return true;
}
}
// Verificar en filesystem
return std::filesystem::exists(filename);
}
auto AssetIntegrated::getSystemPath(const std::string& filename) -> std::string {
// Los archivos de sistema/config siempre van al filesystem
return filename;
}
auto AssetIntegrated::shouldUseResourcePack(const std::string& filepath) -> bool {
// Los archivos que NO van al pack:
// - Archivos de config/ (ahora están fuera de data/)
// - Archivos con absolute=true en assets.txt
// - Archivos de sistema (${SYSTEM_FOLDER})
if (filepath.find("/config/") != std::string::npos ||
filepath.starts_with("config/")) {
return false;
}
if (filepath.find("/data/") != std::string::npos ||
filepath.starts_with("data/")) {
return true;
}
return false;
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <memory>
#include "asset.hpp"
#include "resource_loader.hpp"
// Extensión de Asset que integra ResourceLoader
class AssetIntegrated : public Asset {
public:
// Inicializa Asset con ResourceLoader
static void initWithResourcePack(const std::string& executable_path,
const std::string& resource_pack_path = "resources.pack");
// Carga un archivo usando ResourceLoader como primera opción
static auto loadFile(const std::string& filename) -> std::vector<uint8_t>;
// Verifica si un archivo existe (pack o filesystem)
static auto fileExists(const std::string& filename) -> bool;
// Obtiene la ruta completa para archivos del sistema/config
static auto getSystemPath(const std::string& filename) -> std::string;
private:
static bool resource_pack_enabled;
// Determina si un archivo debe cargarse del pack o del filesystem
static auto shouldUseResourcePack(const std::string& filepath) -> bool;
};

View File

@@ -0,0 +1,849 @@
#include "resource.hpp"
#include <SDL3/SDL.h> // Para SDL_LogInfo, SDL_LogCategory, SDL_LogError, SDL_SetRenderDrawColor, SDL_EventType, SDL_PollEvent, SDL_RenderFillRect, SDL_RenderRect, SDLK_ESCAPE, SDL_Event
#include <array> // Para array
#include <cstdlib> // Para exit
#include <exception> // Para exception
#include <filesystem> // Para exists, path, remove
#include <fstream> // Para basic_ofstream, basic_ios, basic_ostream::write, ios, ofstream
#include <iostream> // Para std::cout
#include <ranges> // Para __find_if_fn, find_if, __find_fn, find
#include <stdexcept> // Para runtime_error
#include <utility> // Para move
#include "asset.hpp" // Para Asset
#include "color.hpp" // Para Color, NO_COLOR_MOD
#include "external/jail_audio.hpp" // Para JA_LoadMusic, JA_LoadSound, JA_DeleteMusic, JA_DeleteSound
#include "lang.hpp" // Para getText
#include "param.hpp" // Para Param, param, ParamPlayer, ParamResource, ParamGame
#include "resource_helper.hpp" // Para loadFile
#include "screen.hpp" // Para Screen
#include "text.hpp" // Para Text
#include "utils.hpp" // Para getFileName
#include "version.h" // Para APP_NAME, GIT_HASH
struct JA_Music_t; // lines 11-11
struct JA_Sound_t; // lines 12-12
// Helper para cargar archivos de audio desde pack o filesystem en memoria
namespace {
struct AudioData {
std::vector<uint8_t> data;
std::string filepath;
};
auto loadAudioData(const std::string& file_path) -> AudioData {
auto resource_data = ResourceHelper::loadFile(file_path);
return AudioData{.data = std::move(resource_data), .filepath = file_path};
}
} // namespace
// Declaraciones de funciones que necesitas implementar en otros archivos
// Singleton
Resource* Resource::instance = nullptr;
// Inicializa la instancia única del singleton con modo de carga
void Resource::init(LoadingMode mode) {
Resource::instance = new Resource(mode);
}
// Libera la instancia
void Resource::destroy() {
delete Resource::instance;
Resource::instance = nullptr;
}
// Obtiene la instancia
auto Resource::get() -> Resource* { return Resource::instance; }
// Constructor con modo de carga
Resource::Resource(LoadingMode mode)
: loading_mode_(mode),
loading_text_(nullptr) {
Screen::get()->show();
if (loading_mode_ == LoadingMode::PRELOAD) {
loading_text_ = Screen::get()->getText();
load();
} else {
// En modo lazy, cargamos lo mínimo indispensable
initResourceLists();
loadEssentialResources();
}
}
// Destructor
Resource::~Resource() {
clear();
}
// Carga los recursos esenciales que siempre se necesitan (modo lazy)
void Resource::loadEssentialResources() {
// Cargar recursos de texto básicos que se usan para crear texturas
loadTextFilesQuiet(); // <- VERSIÓN SILENCIOSA
loadEssentialTextures(); // Ya es silenciosa
createText(); // Crear objetos de texto
createTextTextures(); // Crear texturas generadas (game_text_xxx)
createPlayerTextures(); // Crea las texturas de jugadores con todas sus variantes de paleta
}
// Carga los ficheros de texto del juego (versión silenciosa)
void Resource::loadTextFilesQuiet() {
auto list = Asset::get()->getListByType(Asset::Type::FONT);
for (const auto& l : list) {
auto name = getFileName(l);
// Buscar en nuestra lista y cargar directamente
auto it = std::ranges::find_if(text_files_, [&name](const auto& t) -> auto { return t.name == name; });
if (it != text_files_.end()) {
it->text_file = Text::loadFile(l);
}
}
}
// Carga solo las texturas esenciales (fuentes)
void Resource::loadEssentialTextures() {
const std::vector<std::string> ESSENTIAL_TEXTURES = {
"04b_25.png",
"04b_25_2x.png",
"04b_25_metal.png",
"04b_25_grey.png",
"04b_25_flat.png",
"04b_25_reversed.png",
"04b_25_flat_2x.png",
"04b_25_reversed_2x.png",
"8bithud.png",
"aseprite.png",
"smb2.png",
"smb2_grad.png"};
auto texture_list = Asset::get()->getListByType(Asset::Type::BITMAP);
for (const auto& file : texture_list) {
auto name = getFileName(file);
// Solo cargar texturas esenciales
if (std::ranges::find(ESSENTIAL_TEXTURES, name) != ESSENTIAL_TEXTURES.end()) {
// Buscar en nuestra lista y cargar
auto it = std::ranges::find_if(textures_, [&name](const auto& t) -> auto { return t.name == name; });
if (it != textures_.end()) {
it->texture = std::make_shared<Texture>(Screen::get()->getRenderer(), file);
}
}
}
}
// Inicializa las listas de recursos sin cargar el contenido (modo lazy)
void Resource::initResourceLists() {
// Inicializa lista de sonidos
auto sound_list = Asset::get()->getListByType(Asset::Type::SOUND);
sounds_.clear();
for (const auto& file : sound_list) {
sounds_.emplace_back(getFileName(file));
}
// Inicializa lista de músicas
auto music_list = Asset::get()->getListByType(Asset::Type::MUSIC);
musics_.clear();
for (const auto& file : music_list) {
musics_.emplace_back(getFileName(file));
}
// Inicializa lista de texturas
auto texture_list = Asset::get()->getListByType(Asset::Type::BITMAP);
textures_.clear();
for (const auto& file : texture_list) {
textures_.emplace_back(getFileName(file));
}
// Inicializa lista de ficheros de texto
auto text_file_list = Asset::get()->getListByType(Asset::Type::FONT);
text_files_.clear();
for (const auto& file : text_file_list) {
text_files_.emplace_back(getFileName(file));
}
// Inicializa lista de animaciones
auto animation_list = Asset::get()->getListByType(Asset::Type::ANIMATION);
animations_.clear();
for (const auto& file : animation_list) {
animations_.emplace_back(getFileName(file));
}
// Los demos se cargan directamente sin mostrar progreso (son pocos y pequeños)
loadDemoDataQuiet();
// Inicializa lista de objetos de texto (sin cargar el contenido)
const std::vector<std::string> TEXT_OBJECTS = {
"04b_25",
"04b_25_2x",
"04b_25_metal",
"04b_25_grey",
"04b_25_flat",
"04b_25_reversed",
"04b_25_flat_2x",
"04b_25_reversed_2x",
"8bithud",
"aseprite",
"smb2",
"smb2_grad"};
texts_.clear();
for (const auto& text_name : TEXT_OBJECTS) {
texts_.emplace_back(text_name); // Constructor con nullptr por defecto
}
}
// Obtiene el sonido a partir de un nombre (con carga perezosa)
auto Resource::getSound(const std::string& name) -> JA_Sound_t* {
auto it = std::ranges::find_if(sounds_, [&name](const auto& s) -> auto { return s.name == name; });
if (it != sounds_.end()) {
// Si está en modo lazy y no se ha cargado aún, lo carga ahora
if (loading_mode_ == LoadingMode::LAZY_LOAD && it->sound == nullptr) {
it->sound = loadSoundLazy(name);
}
return it->sound;
}
std::cout << "Error: Sonido no encontrado " << name << '\n';
throw std::runtime_error("Sonido no encontrado: " + name);
}
// Obtiene la música a partir de un nombre (con carga perezosa)
auto Resource::getMusic(const std::string& name) -> JA_Music_t* {
auto it = std::ranges::find_if(musics_, [&name](const auto& m) -> auto { return m.name == name; });
if (it != musics_.end()) {
// Si está en modo lazy y no se ha cargado aún, lo carga ahora
if (loading_mode_ == LoadingMode::LAZY_LOAD && it->music == nullptr) {
it->music = loadMusicLazy(name);
}
return it->music;
}
std::cout << "Error: Música no encontrada " << name << '\n';
throw std::runtime_error("Música no encontrada: " + name);
}
// Obtiene la textura a partir de un nombre (con carga perezosa)
auto Resource::getTexture(const std::string& name) -> std::shared_ptr<Texture> {
auto it = std::ranges::find_if(textures_, [&name](const auto& t) -> auto { return t.name == name; });
if (it != textures_.end()) {
// Si está en modo lazy y no se ha cargado aún, lo carga ahora
if (loading_mode_ == LoadingMode::LAZY_LOAD && it->texture == nullptr) {
it->texture = loadTextureLazy(name);
}
return it->texture;
}
std::cout << "Error: Imagen no encontrada " << name << '\n';
throw std::runtime_error("Imagen no encontrada: " + name);
}
// Obtiene el fichero de texto a partir de un nombre (con carga perezosa)
auto Resource::getTextFile(const std::string& name) -> std::shared_ptr<Text::File> {
auto it = std::ranges::find_if(text_files_, [&name](const auto& t) -> auto { return t.name == name; });
if (it != text_files_.end()) {
// Si está en modo lazy y no se ha cargado aún, lo carga ahora
if (loading_mode_ == LoadingMode::LAZY_LOAD && it->text_file == nullptr) {
it->text_file = loadTextFileLazy(name);
}
return it->text_file;
}
std::cout << "Error: TextFile no encontrado " << name << '\n';
throw std::runtime_error("TextFile no encontrado: " + name);
}
// Obtiene el objeto de texto a partir de un nombre (con carga perezosa)
auto Resource::getText(const std::string& name) -> std::shared_ptr<Text> {
auto it = std::ranges::find_if(texts_, [&name](const auto& t) -> auto { return t.name == name; });
if (it != texts_.end()) {
// Si está en modo lazy y no se ha cargado aún, lo carga ahora
if (loading_mode_ == LoadingMode::LAZY_LOAD && it->text == nullptr) {
it->text = loadTextLazy(name);
}
return it->text;
}
std::cout << "Error: Text no encontrado " << name << '\n';
throw std::runtime_error("Text no encontrado: " + name);
}
// Obtiene la animación a partir de un nombre (con carga perezosa)
auto Resource::getAnimation(const std::string& name) -> AnimationsFileBuffer& {
auto it = std::ranges::find_if(animations_, [&name](const auto& a) -> auto { return a.name == name; });
if (it != animations_.end()) {
// Si está en modo lazy y no se ha cargado aún (vector vacío), lo carga ahora
if (loading_mode_ == LoadingMode::LAZY_LOAD && it->animation.empty()) {
it->animation = loadAnimationLazy(name);
}
return it->animation;
}
std::cout << "Error: Animación no encontrada " << name << '\n';
throw std::runtime_error("Animación no encontrada: " + name);
}
// Obtiene el fichero con los datos para el modo demostración a partir de un índice
auto Resource::getDemoData(int index) -> DemoData& {
if (index < 0 || std::cmp_greater_equal(index, demos_.size())) {
std::cout << "Index " << index << " out of range for demo data (size: " << static_cast<int>(demos_.size()) << ")" << '\n';
static DemoData empty_demo_;
return empty_demo_;
}
return demos_.at(index);
}
// --- Métodos de carga perezosa ---
auto Resource::loadSoundLazy(const std::string& name) -> JA_Sound_t* {
auto sound_list = Asset::get()->getListByType(Asset::Type::SOUND);
for (const auto& file : sound_list) {
if (getFileName(file) == name) {
auto audio_data = loadAudioData(file);
if (!audio_data.data.empty()) {
return JA_LoadSound(audio_data.data.data(), audio_data.data.size());
}
// Fallback a cargar desde disco si no está en pack
return JA_LoadSound(file.c_str());
}
}
return nullptr;
}
auto Resource::loadMusicLazy(const std::string& name) -> JA_Music_t* {
auto music_list = Asset::get()->getListByType(Asset::Type::MUSIC);
for (const auto& file : music_list) {
if (getFileName(file) == name) {
auto audio_data = loadAudioData(file);
if (!audio_data.data.empty()) {
return JA_LoadMusic(audio_data.data.data(), audio_data.data.size());
}
// Fallback a cargar desde disco si no está en pack
return JA_LoadMusic(file.c_str());
}
}
return nullptr;
}
auto Resource::loadTextureLazy(const std::string& name) -> std::shared_ptr<Texture> {
auto texture_list = Asset::get()->getListByType(Asset::Type::BITMAP);
for (const auto& file : texture_list) {
if (getFileName(file) == name) {
return std::make_shared<Texture>(Screen::get()->getRenderer(), file);
}
}
return nullptr;
}
auto Resource::loadTextFileLazy(const std::string& name) -> std::shared_ptr<Text::File> {
auto text_file_list = Asset::get()->getListByType(Asset::Type::FONT);
for (const auto& file : text_file_list) {
if (getFileName(file) == name) {
return Text::loadFile(file);
}
}
return nullptr;
}
auto Resource::loadTextLazy(const std::string& name) -> std::shared_ptr<Text> {
// Mapeo de objetos de texto a sus recursos
struct TextMapping {
std::string key;
std::string texture_file;
std::string text_file;
};
const std::vector<TextMapping> TEXT_MAPPINGS = {
{.key = "04b_25", .texture_file = "04b_25.png", .text_file = "04b_25.txt"},
{.key = "04b_25_2x", .texture_file = "04b_25_2x.png", .text_file = "04b_25_2x.txt"},
{.key = "04b_25_metal", .texture_file = "04b_25_metal.png", .text_file = "04b_25.txt"},
{.key = "04b_25_grey", .texture_file = "04b_25_grey.png", .text_file = "04b_25.txt"},
{.key = "04b_25_flat", .texture_file = "04b_25_flat.png", .text_file = "04b_25.txt"},
{.key = "04b_25_reversed", .texture_file = "04b_25_reversed.png", .text_file = "04b_25.txt"},
{.key = "04b_25_flat_2x", .texture_file = "04b_25_flat_2x.png", .text_file = "04b_25_2x.txt"},
{.key = "04b_25_reversed_2x", .texture_file = "04b_25_reversed_2x.png", .text_file = "04b_25_2x.txt"},
{.key = "8bithud", .texture_file = "8bithud.png", .text_file = "8bithud.txt"},
{.key = "aseprite", .texture_file = "aseprite.png", .text_file = "aseprite.txt"},
{.key = "smb2", .texture_file = "smb2.png", .text_file = "smb2.txt"},
{.key = "smb2_grad", .texture_file = "smb2_grad.png", .text_file = "smb2.txt"}};
for (const auto& mapping : TEXT_MAPPINGS) {
if (mapping.key == name) {
// Cargar las dependencias automáticamente
auto texture = getTexture(mapping.texture_file); // Esto cargará la textura si no está cargada
auto text_file = getTextFile(mapping.text_file); // Esto cargará el archivo de texto si no está cargado
if (texture && text_file) {
return std::make_shared<Text>(texture, text_file);
}
}
}
return nullptr;
}
auto Resource::loadAnimationLazy(const std::string& name) -> AnimationsFileBuffer {
auto animation_list = Asset::get()->getListByType(Asset::Type::ANIMATION);
for (const auto& file : animation_list) {
if (getFileName(file) == name) {
return loadAnimationsFromFile(file);
}
}
// Si no se encuentra, retorna vector vacío
return AnimationsFileBuffer{};
}
// Vacia todos los vectores de recursos
void Resource::clear() {
clearSounds();
clearMusics();
textures_.clear();
text_files_.clear();
texts_.clear();
animations_.clear();
demos_.clear();
}
// Carga todos los recursos del juego y muestra el progreso de carga
void Resource::load() {
// Prepara la gestión del progreso de carga
calculateTotalResources();
initProgressBar();
// Muerstra la ventana y desactiva el sincronismo vertical
auto* screen = Screen::get();
auto vsync = Screen::getVSync();
screen->setVSync(false);
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "\n** LOADING RESOURCES");
loadSounds(); // Carga sonidos
loadMusics(); // Carga músicas
loadTextures(); // Carga texturas
loadTextFiles(); // Carga ficheros de texto
loadAnimations(); // Carga animaciones
loadDemoData(); // Carga datos de demo
createText(); // Crea objetos de texto
createTextTextures(); // Crea texturas a partir de texto
createPlayerTextures(); // Crea las texturas de jugadores con todas sus variantes de paleta
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "\n** RESOURCES LOADED");
// Restablece el sincronismo vertical a su valor original
screen->setVSync(vsync);
}
// Recarga todos los recursos (limpia y vuelve a cargar)
void Resource::reload() {
clear();
if (loading_mode_ == LoadingMode::PRELOAD) {
load();
} else {
initResourceLists();
}
}
// Carga los sonidos del juego
void Resource::loadSounds() {
auto list = Asset::get()->getListByType(Asset::Type::SOUND);
sounds_.clear();
for (const auto& l : list) {
auto name = getFileName(l);
updateLoadingProgress(name);
auto audio_data = loadAudioData(l);
JA_Sound_t* sound = nullptr;
if (!audio_data.data.empty()) {
sound = JA_LoadSound(audio_data.data.data(), audio_data.data.size());
} else {
sound = JA_LoadSound(l.c_str());
}
if (sound == nullptr) {
std::cout << "Sound load failed: " << name << '\n';
}
sounds_.emplace_back(name, sound);
}
}
// Carga las músicas del juego
void Resource::loadMusics() {
auto list = Asset::get()->getListByType(Asset::Type::MUSIC);
musics_.clear();
for (const auto& l : list) {
auto name = getFileName(l);
updateLoadingProgress(name);
auto audio_data = loadAudioData(l);
JA_Music_t* music = nullptr;
if (!audio_data.data.empty()) {
music = JA_LoadMusic(audio_data.data.data(), audio_data.data.size());
} else {
music = JA_LoadMusic(l.c_str());
}
if (music == nullptr) {
std::cout << "Music load failed: " << name << '\n';
}
musics_.emplace_back(name, music);
}
}
// Carga las texturas del juego
void Resource::loadTextures() {
auto list = Asset::get()->getListByType(Asset::Type::BITMAP);
textures_.clear();
for (const auto& l : list) {
auto name = getFileName(l);
updateLoadingProgress(name);
textures_.emplace_back(name, std::make_shared<Texture>(Screen::get()->getRenderer(), l));
}
}
// Carga los ficheros de texto del juego
void Resource::loadTextFiles() {
auto list = Asset::get()->getListByType(Asset::Type::FONT);
text_files_.clear();
for (const auto& l : list) {
auto name = getFileName(l);
updateLoadingProgress(name);
text_files_.emplace_back(name, Text::loadFile(l));
}
}
// Carga las animaciones del juego
void Resource::loadAnimations() {
auto list = Asset::get()->getListByType(Asset::Type::ANIMATION);
animations_.clear();
for (const auto& l : list) {
auto name = getFileName(l);
updateLoadingProgress(name);
animations_.emplace_back(name, loadAnimationsFromFile(l));
}
}
// Carga los datos para el modo demostración
void Resource::loadDemoData() {
auto list = Asset::get()->getListByType(Asset::Type::DEMODATA);
demos_.clear();
for (const auto& l : list) {
auto name = getFileName(l);
updateLoadingProgress(name);
demos_.emplace_back(loadDemoDataFromFile(l));
}
}
// Crea las texturas de jugadores con todas sus variantes de paleta
void Resource::createPlayerTextures() {
// Configuración de jugadores y sus paletas
struct PlayerConfig {
std::string base_texture;
std::vector<std::string> palette_files;
std::string name_prefix;
};
std::vector<PlayerConfig> players = {
{.base_texture = "player1.gif", .palette_files = {"player1_coffee1.pal", "player1_coffee2.pal", "player1_invencible.pal"}, .name_prefix = "player1"},
{.base_texture = "player2.gif", .palette_files = {"player2_coffee1.pal", "player2_coffee2.pal", "player2_invencible.pal"}, .name_prefix = "player2"}};
// Bucle principal
for (size_t player_idx = 0; player_idx < players.size(); ++player_idx) {
const auto& player = players[player_idx]; // Obtenemos el jugador actual
// Encontrar el archivo original de la textura
std::string texture_file_path;
auto texture_list = Asset::get()->getListByType(Asset::Type::BITMAP);
for (const auto& file : texture_list) {
if (getFileName(file) == player.base_texture) {
texture_file_path = file;
break;
}
}
// Crear las 4 texturas con sus respectivas paletas
for (int palette_idx = 0; palette_idx < 4; ++palette_idx) {
std::shared_ptr<Texture> texture;
if (palette_idx == 0) {
// Textura 0 - usar la ya cargada y modificar solo paleta 0 (default_shirt)
texture = getTexture(player.base_texture);
texture->setPaletteColor(0, 16, param.player.default_shirt[player_idx].darkest.TO_UINT32());
texture->setPaletteColor(0, 17, param.player.default_shirt[player_idx].dark.TO_UINT32());
texture->setPaletteColor(0, 18, param.player.default_shirt[player_idx].base.TO_UINT32());
texture->setPaletteColor(0, 19, param.player.default_shirt[player_idx].light.TO_UINT32());
texture->setPaletteColor(0, 56, param.player.outline_color[player_idx].TO_UINT32());
} else {
// Crear textura nueva desde archivo usando ResourceHelper
texture = std::make_shared<Texture>(Screen::get()->getRenderer(), texture_file_path);
// Añadir todas las paletas
texture->addPaletteFromPalFile(Asset::get()->getPath(player.palette_files[0]));
texture->addPaletteFromPalFile(Asset::get()->getPath(player.palette_files[1]));
texture->addPaletteFromPalFile(Asset::get()->getPath(player.palette_files[2]));
if (palette_idx == 1) {
// Textura 1 - modificar solo paleta 1 (one_coffee_shirt)
texture->setPaletteColor(1, 16, param.player.one_coffee_shirt[player_idx].darkest.TO_UINT32());
texture->setPaletteColor(1, 17, param.player.one_coffee_shirt[player_idx].dark.TO_UINT32());
texture->setPaletteColor(1, 18, param.player.one_coffee_shirt[player_idx].base.TO_UINT32());
texture->setPaletteColor(1, 19, param.player.one_coffee_shirt[player_idx].light.TO_UINT32());
texture->setPaletteColor(1, 56, param.player.outline_color[player_idx].TO_UINT32());
} else if (palette_idx == 2) {
// Textura 2 - modificar solo paleta 2 (two_coffee_shirt)
texture->setPaletteColor(2, 16, param.player.two_coffee_shirt[player_idx].darkest.TO_UINT32());
texture->setPaletteColor(2, 17, param.player.two_coffee_shirt[player_idx].dark.TO_UINT32());
texture->setPaletteColor(2, 18, param.player.two_coffee_shirt[player_idx].base.TO_UINT32());
texture->setPaletteColor(2, 19, param.player.two_coffee_shirt[player_idx].light.TO_UINT32());
texture->setPaletteColor(2, 56, param.player.outline_color[player_idx].TO_UINT32());
}
// Textura 3 (palette_idx == 3) - no modificar nada, usar colores originales
}
// Asignar la paleta correspondiente
texture->setPalette(palette_idx);
// Guardar con nombre específico
std::string texture_name = player.name_prefix + "_pal" + std::to_string(palette_idx);
textures_.emplace_back(texture_name, texture);
}
}
}
// Crea texturas a partir de textos para mostrar puntuaciones y mensajes
void Resource::createTextTextures() {
struct NameAndText {
std::string name;
std::string text;
NameAndText(std::string name_init, std::string text_init)
: name(std::move(name_init)),
text(std::move(text_init)) {}
};
// Texturas de tamaño normal con outline
std::vector<NameAndText> strings1 = {
{"game_text_1000_points", "1.000"},
{"game_text_2500_points", "2.500"},
{"game_text_5000_points", "5.000"},
{"game_text_powerup", Lang::getText("[GAME_TEXT] 4")},
{"game_text_one_hit", Lang::getText("[GAME_TEXT] 5")},
{"game_text_stop", Lang::getText("[GAME_TEXT] 6")},
{"game_text_1000000_points", Lang::getText("[GAME_TEXT] 8")}};
auto text1 = getText("04b_25_enhanced");
for (const auto& s : strings1) {
textures_.emplace_back(s.name, text1->writeDXToTexture(Text::STROKE, s.text, -2, Colors::NO_COLOR_MOD, 1, param.game.item_text_outline_color));
}
// Texturas de tamaño doble
std::vector<NameAndText> strings2 = {
{"game_text_100000_points", "100.000"},
{"game_text_get_ready", Lang::getText("[GAME_TEXT] 7")},
{"game_text_last_stage", Lang::getText("[GAME_TEXT] 3")},
{"game_text_congratulations", Lang::getText("[GAME_TEXT] 1")},
{"game_text_new_record", Lang::getText("[GAME_TEXT] NEW_RECORD")},
{"game_text_game_over", "Game Over"}};
auto text2 = getText("04b_25_2x_enhanced");
for (const auto& s : strings2) {
textures_.emplace_back(s.name, text2->writeDXToTexture(Text::STROKE, s.text, -4, Colors::NO_COLOR_MOD, 1, param.game.item_text_outline_color));
}
}
// Crea los objetos de texto a partir de los archivos de textura y texto
void Resource::createText() {
struct ResourceInfo {
std::string key;
std::string texture_file;
std::string text_file;
std::string white_texture_file; // Textura blanca opcional
ResourceInfo(std::string k, std::string t_file, std::string txt_file, std::string w_file = "")
: key(std::move(k)),
texture_file(std::move(t_file)),
text_file(std::move(txt_file)),
white_texture_file(std::move(w_file)) {}
};
std::vector<ResourceInfo> resources = {
{"04b_25", "04b_25.png", "04b_25.txt"},
{"04b_25_enhanced", "04b_25.png", "04b_25.txt", "04b_25_white.png"}, // Nueva fuente con textura blanca
{"04b_25_white", "04b_25_white.png", "04b_25.txt"},
{"04b_25_2x", "04b_25_2x.png", "04b_25_2x.txt"},
{"04b_25_2x_enhanced", "04b_25_2x.png", "04b_25_2x.txt", "04b_25_2x_white.png"}, // Nueva fuente con textura blanca
{"04b_25_metal", "04b_25_metal.png", "04b_25.txt"},
{"04b_25_grey", "04b_25_grey.png", "04b_25.txt"},
{"04b_25_flat", "04b_25_flat.png", "04b_25.txt"},
{"04b_25_reversed", "04b_25_reversed.png", "04b_25.txt"},
{"04b_25_flat_2x", "04b_25_flat_2x.png", "04b_25_2x.txt"},
{"04b_25_reversed_2x", "04b_25_reversed_2x.png", "04b_25_2x.txt"},
{"8bithud", "8bithud.png", "8bithud.txt"},
{"aseprite", "aseprite.png", "aseprite.txt"},
{"smb2", "smb2.png", "smb2.txt"},
{"smb2_grad", "smb2_grad.png", "smb2.txt"}};
for (const auto& resource : resources) {
if (!resource.white_texture_file.empty()) {
// Crear texto con textura blanca
texts_.emplace_back(resource.key, std::make_shared<Text>(getTexture(resource.texture_file), getTexture(resource.white_texture_file), getTextFile(resource.text_file)));
} else {
// Crear texto normal
texts_.emplace_back(resource.key, std::make_shared<Text>(getTexture(resource.texture_file), getTextFile(resource.text_file)));
}
}
}
// Vacía el vector de sonidos y libera la memoria asociada
void Resource::clearSounds() {
for (auto& sound : sounds_) {
if (sound.sound != nullptr) {
JA_DeleteSound(sound.sound);
sound.sound = nullptr;
}
}
sounds_.clear();
}
// Vacía el vector de músicas y libera la memoria asociada
void Resource::clearMusics() {
for (auto& music : musics_) {
if (music.music != nullptr) {
JA_DeleteMusic(music.music);
music.music = nullptr;
}
}
musics_.clear();
}
// Calcula el número total de recursos a cargar y reinicia el contador de carga
void Resource::calculateTotalResources() {
const std::array<Asset::Type, 6> ASSET_TYPES = {
Asset::Type::SOUND,
Asset::Type::MUSIC,
Asset::Type::BITMAP,
Asset::Type::FONT,
Asset::Type::ANIMATION,
Asset::Type::DEMODATA};
size_t total = 0;
for (const auto& asset_type : ASSET_TYPES) {
auto list = Asset::get()->getListByType(asset_type);
total += list.size();
}
loading_count_ = ResourceCount(total);
}
// Muestra el progreso de carga en pantalla (barra y texto)
void Resource::renderProgress() {
// Obtiene la pantalla y el renderer
auto* screen = Screen::get();
auto* renderer = screen->getRenderer();
// Actualiza la lógica principal de la pantalla (input, etc.)
screen->coreUpdate();
// Inicia el frame y limpia la pantalla
screen->start();
screen->clean();
auto text_color = param.resource.color;
auto bar_color = param.resource.color.DARKEN(100);
const auto TEXT_HEIGHT = loading_text_->getCharacterSize();
// Dibuja el interior de la barra de progreso
SDL_SetRenderDrawColor(renderer, bar_color.r, bar_color.g, bar_color.b, bar_color.a);
SDL_RenderFillRect(renderer, &loading_full_rect_);
// Dibuja el marco de la barra de progreso
SDL_SetRenderDrawColor(renderer, bar_color.r, bar_color.g, bar_color.b, bar_color.a);
SDL_RenderRect(renderer, &loading_wired_rect_);
// Escribe el texto de carga encima de la barra
/*
loading_text_->writeColored(
loading_wired_rect_.x,
loading_wired_rect_.y - 9,
Lang::getText("[RESOURCE] LOADING") + " : " + loading_resource_name_,
text_color);
*/
// Muestra nombre de la aplicación
loading_text_->writeDX(
Text::CENTER | Text::COLOR,
param.game.game_area.center_x,
param.game.game_area.center_y - TEXT_HEIGHT,
spaceBetweenLetters(std::string(Version::APP_NAME)),
1,
text_color);
// Muestra la versión
loading_text_->writeDX(
Text::CENTER | Text::COLOR,
param.game.game_area.center_x,
param.game.game_area.center_y + TEXT_HEIGHT,
"(" + std::string(Version::GIT_HASH) + ")",
1,
text_color);
// Muestra información del monitor desplazada hacia abajo
/*loading_text_->writeColored(
X_PADDING,
Y_PADDING + 18,
screen->getDisplayMonitorName(),
text_color);
loading_text_->writeColored(
X_PADDING,
Y_PADDING + 27,
std::to_string(screen->getDisplayMonitorWidth()) + "x" + std::to_string(screen->getDisplayMonitorHeight()),
text_color);
loading_text_->writeColored(
X_PADDING,
Y_PADDING + 36,
std::to_string(screen->getDisplayMonitorRefreshRate()) + "Hz",
text_color);*/
// Renderiza el frame en pantalla
screen->coreRender();
}
// Carga los datos para el modo demostración (sin mostrar progreso)
void Resource::loadDemoDataQuiet() {
auto list = Asset::get()->getListByType(Asset::Type::DEMODATA);
demos_.clear();
for (const auto& l : list) {
demos_.emplace_back(loadDemoDataFromFile(l));
}
}
// Inicializa los rectangulos que definen la barra de progreso
void Resource::initProgressBar() {
const float BAR_Y_POSITION = param.game.height - BAR_HEIGHT - Y_PADDING;
const float WIRED_BAR_WIDTH = param.game.width - (X_PADDING * 2);
loading_wired_rect_ = {.x = X_PADDING, .y = BAR_Y_POSITION, .w = WIRED_BAR_WIDTH, .h = BAR_HEIGHT};
const float FULL_BAR_WIDTH = WIRED_BAR_WIDTH * loading_count_.getPercentage();
loading_full_rect_ = {.x = X_PADDING, .y = BAR_Y_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT};
}
// Actualiza el progreso de carga y muestra la barra
void Resource::updateLoadingProgress(std::string name) {
loading_resource_name_ = std::move(name);
loading_count_.increase();
updateProgressBar();
renderProgress();
}
// Actualiza la barra de estado
void Resource::updateProgressBar() {
loading_full_rect_.w = loading_wired_rect_.w * loading_count_.getPercentage();
}

View File

@@ -0,0 +1,187 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect
#include <cstddef> // Para size_t
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <utility> // Para move
#include <vector> // Para vector
#include "animated_sprite.hpp" // Para AnimationsFileBuffer
#include "demo.hpp" // Para DemoData
#include "text.hpp" // Para Text, TextFile
#include "texture.hpp" // Para Texture
struct JA_Music_t;
struct JA_Sound_t;
// --- Clase Resource: gestiona todos los recursos del juego (singleton) ---
class Resource {
public:
// --- Enum para el modo de carga ---
enum class LoadingMode {
PRELOAD, // Carga todos los recursos al inicio
LAZY_LOAD // Carga los recursos bajo demanda
};
// --- Métodos de singleton ---
static void init(LoadingMode mode = LoadingMode::PRELOAD); // Inicializa el objeto Resource con modo de carga
static void destroy(); // Libera el objeto Resource
static auto get() -> Resource*; // Obtiene el puntero al objeto Resource
// --- Métodos de acceso a recursos ---
auto getSound(const std::string& name) -> JA_Sound_t*; // Obtiene el sonido por nombre
auto getMusic(const std::string& name) -> JA_Music_t*; // Obtiene la música por nombre
auto getTexture(const std::string& name) -> std::shared_ptr<Texture>; // Obtiene la textura por nombre
auto getTextFile(const std::string& name) -> std::shared_ptr<Text::File>; // Obtiene el fichero de texto por nombre
auto getText(const std::string& name) -> std::shared_ptr<Text>; // Obtiene el objeto de texto por nombre
auto getAnimation(const std::string& name) -> AnimationsFileBuffer&; // Obtiene la animación por nombre
auto getDemoData(int index) -> DemoData&; // Obtiene los datos de demo por índice
// --- Métodos de recarga de recursos ---
void reload(); // Recarga todos los recursos
// --- Método para obtener el modo de carga actual ---
[[nodiscard]] auto getLoadingMode() const -> LoadingMode { return loading_mode_; }
private:
// --- Estructuras para recursos individuales ---
struct ResourceSound {
std::string name; // Nombre del sonido
JA_Sound_t* sound; // Objeto con el sonido
ResourceSound(std::string name, JA_Sound_t* sound = nullptr)
: name(std::move(name)),
sound(sound) {}
};
struct ResourceMusic {
std::string name; // Nombre de la música
JA_Music_t* music; // Objeto con la música
ResourceMusic(std::string name, JA_Music_t* music = nullptr)
: name(std::move(name)),
music(music) {}
};
struct ResourceTexture {
std::string name; // Nombre de la textura
std::shared_ptr<Texture> texture; // Objeto con la textura
ResourceTexture(std::string name, std::shared_ptr<Texture> texture = nullptr)
: name(std::move(name)),
texture(std::move(texture)) {}
};
struct ResourceTextFile {
std::string name; // Nombre del fichero
std::shared_ptr<Text::File> text_file; // Objeto con los descriptores de la fuente de texto
ResourceTextFile(std::string name, std::shared_ptr<Text::File> text_file = nullptr)
: name(std::move(name)),
text_file(std::move(text_file)) {}
};
struct ResourceText {
std::string name; // Nombre del objeto
std::shared_ptr<Text> text; // Objeto de texto
ResourceText(std::string name, std::shared_ptr<Text> text = nullptr)
: name(std::move(name)),
text(std::move(text)) {}
};
struct ResourceAnimation {
std::string name; // Nombre de la animación
AnimationsFileBuffer animation; // Objeto con las animaciones
ResourceAnimation(std::string name, AnimationsFileBuffer animation = {})
: name(std::move(name)),
animation(std::move(animation)) {}
};
// --- Estructura para el progreso de carga ---
struct ResourceCount {
size_t total{0}; // Número total de recursos
size_t loaded{0}; // Número de recursos cargados
ResourceCount() = default;
ResourceCount(size_t total)
: total(total) {}
void add(size_t amount) { loaded += amount; }
void increase() { loaded++; }
[[nodiscard]] auto getPercentage() const -> float {
return total > 0 ? static_cast<float>(loaded) / static_cast<float>(total) : 0.0F;
}
};
// --- Constantes para la pantalla de carga ---
static constexpr float X_PADDING = 60.0F;
static constexpr float Y_PADDING = 20.0F;
static constexpr float BAR_HEIGHT = 5.0F;
static constexpr Color BAR_COLOR = Color(128, 128, 128);
static constexpr Color TEXT_COLOR = Color(255, 255, 255);
// --- Modo de carga ---
LoadingMode loading_mode_;
// --- Vectores de recursos ---
std::vector<ResourceSound> sounds_; // Vector con los sonidos
std::vector<ResourceMusic> musics_; // Vector con las músicas
std::vector<ResourceTexture> textures_; // Vector con las texturas
std::vector<ResourceTextFile> text_files_; // Vector con los ficheros de texto
std::vector<ResourceText> texts_; // Vector con los objetos de texto
std::vector<ResourceAnimation> animations_; // Vector con las animaciones
std::vector<DemoData> demos_; // Vector con los ficheros de datos para el modo demostración
// --- Progreso de carga ---
ResourceCount loading_count_; // Contador de recursos cargados
std::shared_ptr<Text> loading_text_; // Texto para escribir en pantalla
std::string loading_resource_name_; // Nombre del recurso que se está cargando
SDL_FRect loading_wired_rect_;
SDL_FRect loading_full_rect_;
// --- Métodos internos de carga y gestión ---
void loadSounds(); // Carga los sonidos
void loadMusics(); // Carga las músicas
void loadTextures(); // Carga las texturas
void loadTextFiles(); // Carga los ficheros de texto
void loadAnimations(); // Carga las animaciones
void loadDemoData(); // Carga los datos para el modo demostración
void loadDemoDataQuiet(); // Carga los datos de demo sin mostrar progreso (para modo lazy)
void loadEssentialResources(); // Carga recursos esenciales en modo lazy
void loadEssentialTextures(); // Carga solo las texturas esenciales (fuentes)
void loadTextFilesQuiet(); // Carga ficheros de texto sin mostrar progreso (para modo lazy)
void createPlayerTextures(); // Crea las texturas de jugadores con todas sus variantes de paleta
void createTextTextures(); // Crea las texturas a partir de los datos cargados
void createText(); // Crea los objetos de texto
void clear(); // Vacía todos los vectores de recursos
void load(); // Carga todos los recursos
void clearSounds(); // Vacía el vector de sonidos
void clearMusics(); // Vacía el vector de músicas
// --- Métodos para carga perezosa ---
void initResourceLists(); // Inicializa las listas de recursos sin cargar el contenido
static auto loadSoundLazy(const std::string& name) -> JA_Sound_t*; // Carga un sonido específico bajo demanda
static auto loadMusicLazy(const std::string& name) -> JA_Music_t*; // Carga una música específica bajo demanda
static auto loadTextureLazy(const std::string& name) -> std::shared_ptr<Texture>; // Carga una textura específica bajo demanda
static auto loadTextFileLazy(const std::string& name) -> std::shared_ptr<Text::File>; // Carga un fichero de texto específico bajo demanda
auto loadTextLazy(const std::string& name) -> std::shared_ptr<Text>; // Carga un objeto de texto específico bajo demanda
static auto loadAnimationLazy(const std::string& name) -> AnimationsFileBuffer; // Carga una animación específica bajo demanda
// --- Métodos internos para gestionar el progreso ---
void calculateTotalResources(); // Calcula el número de recursos para cargar
void renderProgress(); // Muestra el progreso de carga
void updateLoadingProgress(std::string name); // Actualiza el progreso de carga
void initProgressBar(); // Inicializa los rectangulos que definen la barra de progreso
void updateProgressBar(); // Actualiza la barra de estado
// --- Constructores y destructor privados (singleton) ---
explicit Resource(LoadingMode mode); // Constructor privado con modo de carga
~Resource(); // Destructor privado
// --- Instancia singleton ---
static Resource* instance; // Instancia única de Resource
};

View File

@@ -0,0 +1,98 @@
#include "resource_helper.hpp"
#include <algorithm> // Para replace
#include <cstddef> // Para size_t
#include <fstream> // Para basic_ifstream, basic_ostream, basic_ios, operator<<, ios, basic_istream, endl, operator|, basic_istream::read, basic_istream::seekg, basic_istream::tellg, ifstream, streamsize
#include <iostream> // Para cout
#include "resource_loader.hpp" // Para ResourceLoader
namespace ResourceHelper {
static bool resource_system_initialized = false;
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool {
auto& loader = ResourceLoader::getInstance();
resource_system_initialized = loader.initialize(pack_file, enable_fallback);
if (resource_system_initialized) {
std::cout << "Resource system initialized with pack: " << pack_file << '\n';
} else {
std::cout << "Resource system using fallback mode (filesystem only)" << '\n';
}
return true; // Always return true as fallback is acceptable
}
void shutdownResourceSystem() {
if (resource_system_initialized) {
ResourceLoader::getInstance().shutdown();
resource_system_initialized = false;
}
}
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
if (resource_system_initialized && shouldUseResourcePack(filepath)) {
auto& loader = ResourceLoader::getInstance();
std::string pack_path = getPackPath(filepath);
auto data = loader.loadResource(pack_path);
if (!data.empty()) {
return data;
}
}
// Fallback a filesystem
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
return {};
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> data(file_size);
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
return {};
}
return data;
}
auto shouldUseResourcePack(const std::string& filepath) -> bool {
// Archivos que NO van al pack:
// - config/ (ahora está fuera de data/)
// - archivos absolutos del sistema
if (filepath.find("config/") != std::string::npos) {
return false;
}
// Si contiene "data/" es candidato para el pack
if (filepath.find("data/") != std::string::npos) {
return true;
}
return false;
}
auto getPackPath(const std::string& asset_path) -> std::string {
std::string pack_path = asset_path;
// Normalizar separadores de path a '/'
std::ranges::replace(pack_path, '\\', '/');
// Remover prefijo "data/" si existe
size_t data_pos = pack_path.find("data/");
if (data_pos != std::string::npos) {
pack_path = pack_path.substr(data_pos + 5); // +5 para saltar "data/"
}
// Remover cualquier prefijo de path absoluto
size_t last_data = pack_path.rfind("data/");
if (last_data != std::string::npos) {
pack_path = pack_path.substr(last_data + 5);
}
return pack_path;
}
} // namespace ResourceHelper

View File

@@ -0,0 +1,45 @@
#pragma once
#include <cstdint> // Para uint8_t
#include <filesystem> // Para remove, path
#include <fstream> // Para basic_ofstream, basic_ios, basic_ostream::write, ios, ofstream
#include <string> // Para string, basic_string, hash, operator+, to_string, __str_hash_base
#include <vector> // Para vector
// Helper functions para integrar ResourceLoader con el sistema existente
namespace ResourceHelper {
// Inicializa ResourceLoader (llamar al inicio del programa)
auto initializeResourceSystem(const std::string& pack_file = "resources.pack", bool enable_fallback = true) -> bool;
// Cierra ResourceLoader
void shutdownResourceSystem();
// Carga un archivo usando ResourceLoader o fallback a filesystem
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
// Verifica si un archivo debería cargarse del pack vs filesystem
auto shouldUseResourcePack(const std::string& filepath) -> bool;
// Convierte ruta Asset a ruta relativa para ResourceLoader
auto getPackPath(const std::string& asset_path) -> std::string;
// Wrappea la carga de archivos para mantener compatibilidad
template <typename T>
auto loadResourceFile(const std::string& asset_path, T* (*loader_func)(const char*)) -> T* {
auto data = loadFile(asset_path);
if (data.empty()) {
return loader_func(asset_path.c_str());
}
// Crear archivo temporal para funciones que esperan path
std::string temp_path = "/tmp/ccae_" + std::to_string(std::hash<std::string>{}(asset_path));
std::ofstream temp_file(temp_path, std::ios::binary);
temp_file.write(reinterpret_cast<const char*>(data.data()), data.size());
temp_file.close();
T* result = loader_func(temp_path.c_str());
std::filesystem::remove(temp_path);
return result;
}
} // namespace ResourceHelper

View File

@@ -0,0 +1,133 @@
#include "resource_loader.hpp"
#include <algorithm> // Para replace
#include <filesystem> // Para exists, path, recursive_directory_iterator, directory_entry, relative
#include <fstream> // Para basic_ostream, basic_ifstream, operator<<, basic_ios, endl, ios, basic_istream, operator|, basic_istream::read, basic_istream::seekg, basic_istream::tellg, ifstream, streamsize
#include <iostream> // Para cerr, cout
#include "resource_pack.hpp" // Para ResourcePack
std::unique_ptr<ResourceLoader> ResourceLoader::instance = nullptr;
ResourceLoader::ResourceLoader()
: resource_pack_(nullptr),
fallback_to_files_(true) {}
auto ResourceLoader::getInstance() -> ResourceLoader& {
if (!instance) {
instance = std::unique_ptr<ResourceLoader>(new ResourceLoader());
}
return *instance;
}
ResourceLoader::~ResourceLoader() {
shutdown();
}
auto ResourceLoader::initialize(const std::string& pack_file, bool enable_fallback) -> bool {
shutdown();
fallback_to_files_ = enable_fallback;
pack_path_ = pack_file;
if (std::filesystem::exists(pack_file)) {
resource_pack_ = new ResourcePack();
if (resource_pack_->loadPack(pack_file)) {
return true;
}
delete resource_pack_;
resource_pack_ = nullptr;
std::cerr << "Failed to load resource pack: " << pack_file << '\n';
}
if (fallback_to_files_) {
return true;
}
std::cerr << "Resource pack not found and fallback disabled: " << pack_file << '\n';
return false;
}
void ResourceLoader::shutdown() {
if (resource_pack_ != nullptr) {
delete resource_pack_;
resource_pack_ = nullptr;
}
}
auto ResourceLoader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
if ((resource_pack_ != nullptr) && resource_pack_->hasResource(filename)) {
return resource_pack_->getResource(filename);
}
if (fallback_to_files_) {
return loadFromFile(filename);
}
std::cerr << "Resource not found: " << filename << '\n';
return {};
}
auto ResourceLoader::resourceExists(const std::string& filename) -> bool {
if ((resource_pack_ != nullptr) && resource_pack_->hasResource(filename)) {
return true;
}
if (fallback_to_files_) {
std::string full_path = getDataPath(filename);
return std::filesystem::exists(full_path);
}
return false;
}
auto ResourceLoader::loadFromFile(const std::string& filename) -> std::vector<uint8_t> {
std::string full_path = getDataPath(filename);
std::ifstream file(full_path, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Error: Could not open file: " << full_path << '\n';
return {};
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> data(file_size);
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
std::cerr << "Error: Could not read file: " << full_path << '\n';
return {};
}
return data;
}
auto ResourceLoader::getDataPath(const std::string& filename) -> std::string {
return "data/" + filename;
}
auto ResourceLoader::getLoadedResourceCount() const -> size_t {
if (resource_pack_ != nullptr) {
return resource_pack_->getResourceCount();
}
return 0;
}
auto ResourceLoader::getAvailableResources() const -> std::vector<std::string> {
if (resource_pack_ != nullptr) {
return resource_pack_->getResourceList();
}
std::vector<std::string> result;
if (fallback_to_files_ && std::filesystem::exists("data")) {
for (const auto& entry : std::filesystem::recursive_directory_iterator("data")) {
if (entry.is_regular_file()) {
std::string filename = std::filesystem::relative(entry.path(), "data").string();
std::ranges::replace(filename, '\\', '/');
result.push_back(filename);
}
}
}
return result;
}

View File

@@ -0,0 +1,39 @@
#pragma once
#include <cstddef> // Para size_t
#include <cstdint> // Para uint8_t
#include <memory> // Para unique_ptr
#include <string> // Para string, basic_string
#include <vector> // Para vector
class ResourcePack;
class ResourceLoader {
private:
static std::unique_ptr<ResourceLoader> instance;
ResourcePack* resource_pack_;
std::string pack_path_;
bool fallback_to_files_;
ResourceLoader();
public:
static auto getInstance() -> ResourceLoader&;
~ResourceLoader();
auto initialize(const std::string& pack_file, bool enable_fallback = true) -> bool;
void shutdown();
auto loadResource(const std::string& filename) -> std::vector<uint8_t>;
auto resourceExists(const std::string& filename) -> bool;
void setFallbackToFiles(bool enable) { fallback_to_files_ = enable; }
[[nodiscard]] auto getFallbackToFiles() const -> bool { return fallback_to_files_; }
[[nodiscard]] auto getLoadedResourceCount() const -> size_t;
[[nodiscard]] auto getAvailableResources() const -> std::vector<std::string>;
private:
static auto loadFromFile(const std::string& filename) -> std::vector<uint8_t>;
static auto getDataPath(const std::string& filename) -> std::string;
};

View File

@@ -0,0 +1,228 @@
#include "resource_pack.hpp"
#include <algorithm> // Para replace
#include <array> // Para array
#include <filesystem> // Para path, recursive_directory_iterator, directory_entry, exists, relative
#include <fstream> // Para basic_ifstream, basic_ostream, basic_ofstream, operator<<, basic_ios, basic_istream::read, basic_ostream::write, endl, ios, basic_istream, ifstream, operator|, basic_istream::seekg, basic_istream::tellg, ofstream, streamsize
#include <iostream> // Para cerr
#include <utility> // Para pair
const std::string ResourcePack::DEFAULT_ENCRYPT_KEY = "CCAE_RESOURCES__2024";
ResourcePack::ResourcePack()
: loaded_(false) {}
ResourcePack::~ResourcePack() {
clear();
}
auto ResourcePack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
uint32_t checksum = 0x12345678;
for (unsigned char i : data) {
checksum = ((checksum << 5) + checksum) + i;
}
return checksum;
}
void ResourcePack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) {
return;
}
for (size_t i = 0; i < data.size(); ++i) {
data[i] ^= key[i % key.length()];
}
}
void ResourcePack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
encryptData(data, key);
}
auto ResourcePack::loadPack(const std::string& pack_file) -> bool {
std::ifstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "Error: Could not open pack file: " << pack_file << '\n';
return false;
}
std::array<char, 4> header;
file.read(header.data(), 4);
if (std::string(header.data(), 4) != "CCAE") {
std::cerr << "Error: Invalid pack file format" << '\n';
return false;
}
uint32_t version;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != 1) {
std::cerr << "Error: Unsupported pack version: " << version << '\n';
return false;
}
uint32_t resource_count;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
resources_.clear();
resources_.reserve(resource_count);
for (uint32_t i = 0; i < resource_count; ++i) {
uint32_t filename_length;
file.read(reinterpret_cast<char*>(&filename_length), sizeof(filename_length));
std::string filename(filename_length, '\0');
file.read(filename.data(), filename_length);
ResourceEntry entry;
entry.filename = filename;
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
resources_[filename] = entry;
}
uint64_t data_size;
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
data_.resize(data_size);
file.read(reinterpret_cast<char*>(data_.data()), data_size);
decryptData(data_, DEFAULT_ENCRYPT_KEY);
loaded_ = true;
return true;
}
auto ResourcePack::savePack(const std::string& pack_file) -> bool {
std::ofstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "Error: Could not create pack file: " << pack_file << '\n';
return false;
}
file.write("CCAE", 4);
uint32_t version = 1;
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
auto resource_count = static_cast<uint32_t>(resources_.size());
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
for (const auto& [filename, entry] : resources_) {
auto filename_length = static_cast<uint32_t>(filename.length());
file.write(reinterpret_cast<const char*>(&filename_length), sizeof(filename_length));
file.write(filename.c_str(), filename_length);
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
}
std::vector<uint8_t> encrypted_data = data_;
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
uint64_t data_size = encrypted_data.size();
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), data_size);
return true;
}
auto ResourcePack::addFile(const std::string& filename, const std::string& filepath) -> bool {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Error: Could not open file: " << filepath << '\n';
return false;
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> file_data(file_size);
if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) {
std::cerr << "Error: Could not read file: " << filepath << '\n';
return false;
}
ResourceEntry entry;
entry.filename = filename;
entry.offset = data_.size();
entry.size = file_data.size();
entry.checksum = calculateChecksum(file_data);
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[filename] = entry;
return true;
}
auto ResourcePack::addDirectory(const std::string& directory) -> bool {
if (!std::filesystem::exists(directory)) {
std::cerr << "Error: Directory does not exist: " << directory << '\n';
return false; // NOLINT(readability-simplify-boolean-expr)
}
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
if (entry.is_regular_file()) {
std::string filepath = entry.path().string();
std::string filename = std::filesystem::relative(entry.path(), directory).string();
std::ranges::replace(filename, '\\', '/');
if (!addFile(filename, filepath)) {
return false;
}
}
}
return true;
}
auto ResourcePack::getResource(const std::string& filename) -> std::vector<uint8_t> {
auto it = resources_.find(filename);
if (it == resources_.end()) {
std::cerr << "Error: Resource not found: " << filename << '\n';
return {};
}
const ResourceEntry& entry = it->second;
if (entry.offset + entry.size > data_.size()) {
std::cerr << "Error: Invalid resource data: " << filename << '\n';
return {};
}
std::vector<uint8_t> result(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
uint32_t checksum = calculateChecksum(result);
if (checksum != entry.checksum) {
std::cerr << "Warning: Checksum mismatch for resource: " << filename << '\n';
}
return result;
}
auto ResourcePack::hasResource(const std::string& filename) const -> bool {
return resources_.contains(filename);
}
void ResourcePack::clear() {
resources_.clear();
data_.clear();
loaded_ = false;
}
auto ResourcePack::getResourceCount() const -> size_t {
return resources_.size();
}
auto ResourcePack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> result;
result.reserve(resources_.size());
for (const auto& [filename, entry] : resources_) {
result.push_back(filename);
}
return result;
}

View File

@@ -0,0 +1,44 @@
#pragma once
#include <cstddef> // Para size_t
#include <cstdint> // Para uint8_t, uint32_t, uint64_t
#include <string> // Para string, basic_string, hash
#include <unordered_map> // Para unordered_map
#include <vector> // Para vector
struct ResourceEntry {
std::string filename;
uint64_t offset;
uint64_t size;
uint32_t checksum;
};
class ResourcePack {
private:
std::unordered_map<std::string, ResourceEntry> resources_;
std::vector<uint8_t> data_;
bool loaded_;
static auto calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t;
static void encryptData(std::vector<uint8_t>& data, const std::string& key);
static void decryptData(std::vector<uint8_t>& data, const std::string& key);
public:
ResourcePack();
~ResourcePack();
auto loadPack(const std::string& pack_file) -> bool;
auto savePack(const std::string& pack_file) -> bool;
auto addFile(const std::string& filename, const std::string& filepath) -> bool;
auto addDirectory(const std::string& directory) -> bool;
[[nodiscard]] auto getResource(const std::string& filename) -> std::vector<uint8_t>;
[[nodiscard]] auto hasResource(const std::string& filename) const -> bool;
void clear();
[[nodiscard]] auto getResourceCount() const -> size_t;
[[nodiscard]] auto getResourceList() const -> std::vector<std::string>;
static const std::string DEFAULT_ENCRYPT_KEY;
};

View File

@@ -0,0 +1,215 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_ScaleMode
#include <array>
#include "ui/notifier.hpp" // Para Notifier::Position
namespace Defaults::Game {
constexpr float WIDTH = 320.0F;
constexpr float HEIGHT = 256.0F;
constexpr int NAME_ENTRY_IDLE_TIME = 10;
constexpr int NAME_ENTRY_TOTAL_TIME = 60;
constexpr bool HIT_STOP = false;
constexpr int HIT_STOP_MS = 500;
constexpr const char* ITEM_TEXT_OUTLINE_COLOR = "FFFFFF00";
constexpr float PLAY_AREA_X = 0.0F;
constexpr float PLAY_AREA_Y = 0.0F;
constexpr float PLAY_AREA_W = 320.0F;
constexpr float PLAY_AREA_H = 216.0F;
} // namespace Defaults::Game
namespace Defaults::Fade {
constexpr const char* COLOR = "1F2B30";
constexpr float NUM_SQUARES_WIDTH = 160.0F;
constexpr float NUM_SQUARES_HEIGHT = 128.0F;
constexpr int RANDOM_SQUARES_DURATION_MS = 1;
constexpr int POST_DURATION_MS = 80;
constexpr float VENETIAN_SIZE = 12.0F;
} // namespace Defaults::Fade
namespace Defaults::Scoreboard {
constexpr float RECT_X = 0.0F;
constexpr float RECT_Y = 216.0F;
constexpr float RECT_W = 320.0F;
constexpr float RECT_H = 40.0F;
constexpr bool SEPARATOR_AUTOCOLOR = true;
constexpr const char* SEPARATOR_COLOR = "0D1A2B";
constexpr const char* EASY_COLOR = "4B692F";
constexpr const char* NORMAL_COLOR = "2E3F47";
constexpr const char* HARD_COLOR = "76428A";
constexpr bool TEXT_AUTOCOLOR = true;
constexpr const char* TEXT_COLOR1 = "FFFFFF";
constexpr const char* TEXT_COLOR2 = "FFFFFF";
constexpr int SKIP_COUNTDOWN_VALUE = 8;
} // namespace Defaults::Scoreboard
namespace Defaults::Title {
constexpr int PRESS_START_POSITION = 180;
constexpr float DURATION_S = 14.0F;
constexpr int ARCADE_EDITION_POSITION = 123;
constexpr int TITLE_C_C_POSITION = 80;
constexpr const char* BG_COLOR = "41526F";
} // namespace Defaults::Title
namespace Defaults::Background {
constexpr const char* ATTENUATE_COLOR = "FFFFFF00";
} // namespace Defaults::Background
namespace Defaults::Balloon {
struct BalloonSettings {
float vel;
float grav;
constexpr BalloonSettings(float v, float g)
: vel(v),
grav(g) {}
};
constexpr std::array<BalloonSettings, 4> SETTINGS = {{
BalloonSettings(165.0F, 320.0F),
BalloonSettings(222.0F, 360.0F),
BalloonSettings(282.0F, 360.0F),
BalloonSettings(327.0F, 360.0F),
}};
constexpr std::array<const char*, 4> COLORS = {
"blue",
"orange",
"red",
"green"};
constexpr bool BOUNCING_SOUND = false;
} // namespace Defaults::Balloon
namespace Defaults::Notification {
constexpr Notifier::Position POS_V = Notifier::Position::TOP;
constexpr Notifier::Position POS_H = Notifier::Position::LEFT;
constexpr bool SOUND = false;
constexpr const char* COLOR = "303030";
} // namespace Defaults::Notification
namespace Defaults::ServiceMenu {
constexpr const char* TITLE_COLOR = "99FF62";
constexpr const char* TEXT_COLOR = "FFFFFF";
constexpr const char* SELECTED_COLOR = "FFDC44";
constexpr const char* BG_COLOR = "000F00F5";
constexpr bool DROP_SHADOW = false;
} // namespace Defaults::ServiceMenu
namespace Defaults::ServiceMenu::WindowMessage {
constexpr const char* BG_COLOR = "141E32F0";
constexpr const char* BORDER_COLOR = "6496C8FF";
constexpr const char* TITLE_COLOR = "6496C8FF";
constexpr const char* TEXT_COLOR = "DCDCDCFF";
constexpr float PADDING = 15.0F;
constexpr float LINE_SPACING = 5.0F;
constexpr float TITLE_SEPARATOR_SPACING = 10.0F;
constexpr float MIN_WIDTH = 250.0F;
constexpr float MIN_HEIGHT = 32.0F;
constexpr float MAX_WIDTH_RATIO = 0.8F;
constexpr float MAX_HEIGHT_RATIO = 0.8F;
constexpr float TEXT_SAFETY_MARGIN = 15.0F;
constexpr float ANIMATION_DURATION = 0.3F;
} // namespace Defaults::ServiceMenu::WindowMessage
namespace Defaults::Intro {
constexpr const char* BG_COLOR = "4664BD";
constexpr const char* CARD_COLOR = "CBDBFC";
constexpr const char* SHADOW_COLOR = "00000080";
constexpr int TEXT_DISTANCE_FROM_BOTTOM = 48;
} // namespace Defaults::Intro
namespace Defaults::Debug {
constexpr const char* COLOR = "00FFFF";
} // namespace Defaults::Debug
namespace Defaults::Resource {
constexpr const char* COLOR = "FFFFFF";
} // namespace Defaults::Resource
namespace Defaults::Tabe {
constexpr float MIN_SPAWN_TIME = 2.0F;
constexpr float MAX_SPAWN_TIME = 3.0F;
} // namespace Defaults::Tabe
namespace Defaults::Player::DefaultShirt {
constexpr const char* PLAYER0_DARKEST = "028ECFFF";
constexpr const char* PLAYER0_DARK = "0297DBFF";
constexpr const char* PLAYER0_BASE = "029FE8FF";
constexpr const char* PLAYER0_LIGHT = "03A9F4FF";
constexpr const char* PLAYER1_DARKEST = "8E8E8EFF";
constexpr const char* PLAYER1_DARK = "AEADADFF";
constexpr const char* PLAYER1_BASE = "E4E4E4FF";
constexpr const char* PLAYER1_LIGHT = "F7F1F1FF";
} // namespace Defaults::Player::DefaultShirt
namespace Defaults::Player::OneCoffeeShirt {
constexpr const char* PLAYER0_DARKEST = "3D9C70FF";
constexpr const char* PLAYER0_DARK = "4FA370FF";
constexpr const char* PLAYER0_BASE = "5DDE70FF";
constexpr const char* PLAYER0_LIGHT = "7DF25CFF";
constexpr const char* PLAYER1_DARKEST = "2E8B57FF";
constexpr const char* PLAYER1_DARK = "3CB371FF";
constexpr const char* PLAYER1_BASE = "48D181FF";
constexpr const char* PLAYER1_LIGHT = "55EF8DFF";
} // namespace Defaults::Player::OneCoffeeShirt
namespace Defaults::Player::TwoCoffeeShirt {
constexpr const char* PLAYER0_DARKEST = "D6A41AFF";
constexpr const char* PLAYER0_DARK = "E3AE1BFF";
constexpr const char* PLAYER0_BASE = "EFB71DFF";
constexpr const char* PLAYER0_LIGHT = "FCC11EFF";
constexpr const char* PLAYER1_DARKEST = "E08500FF";
constexpr const char* PLAYER1_DARK = "FA7D00FF";
constexpr const char* PLAYER1_BASE = "FAA200FF";
constexpr const char* PLAYER1_LIGHT = "FA8500FF";
} // namespace Defaults::Player::TwoCoffeeShirt
namespace Defaults::Player::OutlineColor {
constexpr const char* PLAYER0 = "66323FFF";
constexpr const char* PLAYER1 = "422028FF";
} // namespace Defaults::Player::OutlineColor
namespace Defaults::Window {
constexpr const char* CAPTION = "© 2025 Coffee Crisis Arcade Edition — JailDesigner";
constexpr int ZOOM = 2;
constexpr int MAX_ZOOM = 2;
} // namespace Defaults::Window
namespace Defaults::Video {
constexpr SDL_ScaleMode SCALE_MODE = SDL_ScaleMode::SDL_SCALEMODE_NEAREST;
constexpr bool FULLSCREEN = false;
constexpr bool VSYNC = true;
constexpr bool INTEGER_SCALE = true;
constexpr bool GPU_ACCELERATION = true;
constexpr bool SHADER_ENABLED = false;
constexpr bool SUPERSAMPLING = false;
constexpr bool LINEAR_UPSCALE = false;
constexpr int DOWNSCALE_ALGO = 1;
} // namespace Defaults::Video
namespace Defaults::Music {
constexpr bool ENABLED = true;
constexpr int VOLUME = 100;
} // namespace Defaults::Music
namespace Defaults::Sound {
constexpr bool ENABLED = true;
constexpr int VOLUME = 100;
} // namespace Defaults::Sound
namespace Defaults::Audio {
constexpr bool ENABLED = true;
constexpr int VOLUME = 100;
} // namespace Defaults::Audio
namespace Defaults::Settings {
constexpr bool AUTOFIRE = true;
constexpr bool SHUTDOWN_ENABLED = false;
constexpr const char* PARAMS_FILE = "param_320x256.txt";
} // namespace Defaults::Settings

View File

@@ -0,0 +1,68 @@
#include "demo.hpp"
#include <SDL3/SDL.h> // Para SDL_IOStream, SDL_IOFromConstMem, SDL_IOFromFile, SDL_ReadIO, SDL_WriteIO, SDL_CloseIO
#include <iostream> // Para std::cout
#include <stdexcept> // Para runtime_error
#include "resource_helper.hpp" // Para ResourceHelper
#include "utils.hpp" // Para getFileName
// Carga el fichero de datos para la demo
auto loadDemoDataFromFile(const std::string& file_path) -> DemoData {
DemoData dd;
SDL_IOStream* file = nullptr;
// Intentar cargar desde ResourceHelper primero
auto resource_data = ResourceHelper::loadFile(file_path);
if (!resource_data.empty()) {
file = SDL_IOFromConstMem(resource_data.data(), resource_data.size());
} else {
// Fallback a filesystem directo
file = SDL_IOFromFile(file_path.c_str(), "r+b");
}
if (file == nullptr) {
std::cout << "Error: Fichero no encontrado " << file_path << '\n';
throw std::runtime_error("Fichero no encontrado: " + file_path);
}
// Lee todos los datos del fichero y los deja en el destino
for (int i = 0; i < TOTAL_DEMO_DATA; ++i) {
DemoKeys dk = DemoKeys();
SDL_ReadIO(file, &dk, sizeof(DemoKeys));
dd.push_back(dk);
}
// Cierra el fichero
SDL_CloseIO(file);
return dd;
}
#ifdef RECORDING
// Guarda el fichero de datos para la demo
bool saveDemoFile(const std::string& file_path, const DemoData& dd) {
auto success = true;
auto file = SDL_IOFromFile(file_path.c_str(), "w+b");
if (file) {
// Guarda los datos
for (const auto& data : dd) {
if (SDL_WriteIO(file, &data, sizeof(DemoKeys)) != sizeof(DemoKeys)) {
std::cout << "Error al escribir el fichero " << getFileName(file_path) << '\n';
success = false;
break;
}
}
// Cierra el fichero
SDL_CloseIO(file);
} else {
std::cout << "Error: Unable to save " << getFileName(file_path) << " file! " << SDL_GetError() << '\n';
success = false;
}
return success;
}
#endif // RECORDING

View File

@@ -0,0 +1,55 @@
#pragma once
#include <SDL3/SDL.h> // Para Uint8
#include <string> // Para string
#include <vector> // Para vector
// --- Constantes ---
constexpr int TOTAL_DEMO_DATA = 2000;
// --- Estructuras ---
struct DemoKeys {
Uint8 left;
Uint8 right;
Uint8 no_input;
Uint8 fire;
Uint8 fire_left;
Uint8 fire_right;
explicit DemoKeys(Uint8 l = 0, Uint8 r = 0, Uint8 ni = 0, Uint8 f = 0, Uint8 fl = 0, Uint8 fr = 0)
: left(l),
right(r),
no_input(ni),
fire(f),
fire_left(fl),
fire_right(fr) {}
};
// --- Tipos ---
using DemoData = std::vector<DemoKeys>;
struct Demo {
bool enabled = false; // Indica si está activo el modo demo
bool recording = false; // Indica si está activado el modo para grabar la demo
float elapsed_s = 0.0F; // Segundos transcurridos de demo
int index = 0; // Contador para el modo demo
DemoKeys keys; // Variable con las pulsaciones de teclas del modo demo
std::vector<DemoData> data; // Vector con diferentes sets de datos con los movimientos para la demo
Demo() = default;
Demo(bool e, bool r, int c, const DemoKeys& k, const std::vector<DemoData>& d)
: enabled(e),
recording(r),
index(c),
keys(k),
data(d) {}
};
// --- Funciones ---
auto loadDemoDataFromFile(const std::string& file_path) -> DemoData;
#ifdef RECORDING
bool saveDemoFile(const std::string& file_path, const DemoData& dd);
#endif

View File

@@ -0,0 +1,469 @@
// IWYU pragma: no_include <bits/chrono.h>
#include "director.hpp"
#include <SDL3/SDL.h> // Para SDL_SetLogPriority, SDL_LogCategory, SDL_LogPriority, SDL_Quit
#include <cstdlib> // Para srand, exit, rand, EXIT_FAILURE
#include <ctime> // Para time
#include <fstream> // Para ifstream, ofstream
#include <iostream> // Para basic_ostream, operator<<, cerr
#include <memory> // Para make_unique, unique_ptr
#include <stdexcept> // Para runtime_error
#include <string> // Para allocator, basic_string, char_traits, operator+, string, operator==
#include "asset.hpp" // Para Asset
#include "audio.hpp" // Para Audio
#include "external/fkyaml_node.hpp" // Para fkyaml::node
#include "global_events.hpp" // Para GlobalEvents::handle
#include "input.hpp" // Para Input
#include "lang.hpp" // Para setLanguage
#include "manage_hiscore_table.hpp" // Para ManageHiScoreTable
#include "options.hpp" // Para Settings, loadFromFile, saveToFile, settings, setConfigFile, setControllersFile
#include "param.hpp" // Para loadParamsFromFile
#include "player.hpp" // Para Player
#include "resource.hpp" // Para Resource
#include "resource_helper.hpp" // Para initializeResourceSystem
#include "screen.hpp" // Para Screen
#include "section.hpp" // Para Name, Options, name, options, AttractMode, attract_mode
#include "sections/credits.hpp" // Para Credits
#include "sections/game.hpp" // Para Game
#include "sections/hiscore_table.hpp" // Para HiScoreTable
#include "sections/instructions.hpp" // Para Instructions
#include "sections/intro.hpp" // Para Intro
#include "sections/logo.hpp" // Para Logo
#include "sections/title.hpp" // Para Title
#include "shutdown.hpp" // Para resultToString, shutdownSystem, ShutdownResult
#include "system_utils.hpp" // Para createApplicationFolder, resultToString, Result
#include "ui/notifier.hpp" // Para Notifier
#include "ui/service_menu.hpp" // Para ServiceMenu
// Constructor
Director::Director() {
Section::attract_mode = Section::AttractMode::TITLE_TO_DEMO;
// Establece el nivel de prioridad de la categoría de registro
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO);
SDL_SetLogPriority(SDL_LOG_CATEGORY_TEST, SDL_LOG_PRIORITY_ERROR);
// Inicia la semilla aleatoria usando el tiempo actual en segundos
std::srand(static_cast<unsigned int>(std::time(nullptr)));
std::cout << "Game start\n";
// Obtener la ruta del ejecutable desde SDL
const char* base_path = SDL_GetBasePath();
executable_path_ = (base_path != nullptr) ? base_path : "";
// Crea la carpeta del sistema donde guardar los datos persistentes
createSystemFolder("jailgames");
createSystemFolder("jailgames/coffee_crisis_arcade_edition");
// Establecer sección inicial según modo de compilación
#ifdef RECORDING
Section::name = Section::Name::GAME;
Section::options = Section::Options::GAME_PLAY_1P;
#elif _DEBUG
loadDebugConfig();
#else
Section::name = Section::Name::LOGO;
Section::options = Section::Options::NONE;
#endif
init();
}
Director::~Director() {
// Libera las secciones primero: sus destructores pueden tocar Audio/Resource/Screen,
// que close() destruye a continuación.
resetActiveSection();
close();
}
// Inicializa todo
void Director::init() {
// Configuración inicial de parametros
Asset::init(executable_path_); // Inicializa el sistema de gestión de archivos
// Determinar ruta del pack según la plataforma
#ifdef MACOS_BUNDLE
std::string pack_path = executable_path_ + "../Resources/resources.pack";
#else
std::string pack_path = executable_path_ + "resources.pack";
#endif
// Inicializar sistema de recursos con o sin fallback según el tipo de build
#ifdef RELEASE_BUILD
// Release: Sin fallback - Solo resources.pack (estricto)
ResourceHelper::initializeResourceSystem(pack_path, false);
#else
// Desarrollo: Con fallback - Puede usar data/ si falta el pack (flexible)
ResourceHelper::initializeResourceSystem(pack_path, true);
#endif
loadAssets(); // Crea el índice de archivos
Input::init(Asset::get()->getPath("gamecontrollerdb.txt"), Asset::get()->getPath("controllers.json")); // Carga configuración de controles
Options::setConfigFile(Asset::get()->getPath("config.yaml")); // Establece el fichero de configuración
Options::setControllersFile(Asset::get()->getPath("controllers.json")); // Establece el fichero de configuración de mandos
Options::setPostFXFile(Asset::get()->getPath("postfx.yaml")); // Establece el fichero de presets PostFX
Options::setCrtPiFile(Asset::get()->getPath("crtpi.yaml")); // Establece el fichero de presets CrtPi
Options::loadFromFile(); // Carga el archivo de configuración
Options::loadPostFXFromFile(); // Carga los presets PostFX
Options::loadCrtPiFromFile(); // Carga los presets CrtPi
loadParams(); // Carga los parámetros del programa
loadScoreFile(); // Carga el archivo de puntuaciones
// Inicialización de subsistemas principales
Lang::setLanguage(Options::settings.language); // Carga el archivo de idioma
Screen::init(); // Inicializa la pantalla y el sistema de renderizado
Audio::init(); // Activa el sistema de audio
#ifdef _DEBUG
Resource::init(debug_config.resource_loading == "lazy" ? Resource::LoadingMode::LAZY_LOAD : Resource::LoadingMode::PRELOAD);
#else
Resource::init(Resource::LoadingMode::PRELOAD);
#endif
ServiceMenu::init(); // Inicializa el menú de servicio
Notifier::init(std::string(), Resource::get()->getText("8bithud")); // Inicialización del sistema de notificaciones
Screen::get()->getSingletons(); // Obtiene los punteros al resto de singletones
}
// Cierra todo y libera recursos del sistema y de los singletons
void Director::close() {
// Guarda las opciones actuales en el archivo de configuración
Options::saveToFile();
// Libera los singletons y recursos en orden inverso al de inicialización
Notifier::destroy(); // Libera el sistema de notificaciones
ServiceMenu::destroy(); // Libera el sistema de menú de servicio
Input::destroy(); // Libera el sistema de entrada
Resource::destroy(); // Libera el sistema de recursos gráficos y de texto
Audio::destroy(); // Libera el sistema de audio
Screen::destroy(); // Libera el sistema de pantalla y renderizado
Asset::destroy(); // Libera el gestor de archivos
std::cout << "\nBye!\n";
// Libera todos los recursos de SDL
SDL_Quit();
// Apaga el sistema
shutdownSystem(Section::options == Section::Options::SHUTDOWN);
}
// Carga los parametros
void Director::loadParams() {
// Carga los parametros para configurar el juego
#ifdef ANBERNIC
const std::string PARAM_FILE_PATH = Asset::get()->getPath("param_320x240.txt");
#else
const std::string PARAM_FILE_PATH = Asset::get()->getPath(Options::settings.params_file);
#endif
loadParamsFromFile(PARAM_FILE_PATH);
}
// Carga el fichero de puntuaciones
void Director::loadScoreFile() {
auto manager = std::make_unique<ManageHiScoreTable>(Options::settings.hi_score_table);
#ifdef _DEBUG
manager->clear();
#else
manager->loadFromFile(Asset::get()->getPath("score.bin"));
#endif
}
// Carga el indice de ficheros desde un fichero
void Director::loadAssets() {
#ifdef MACOS_BUNDLE
const std::string PREFIX = "/../Resources";
#else
const std::string PREFIX;
#endif
// Cargar la configuración de assets (también aplicar el prefijo al archivo de configuración)
std::string config_path = executable_path_ + PREFIX + "/config/assets.txt";
Asset::get()->loadFromFile(config_path, PREFIX, system_folder_);
// Si falta algun fichero, sale del programa
if (!Asset::get()->check()) {
throw std::runtime_error("Falta algun fichero");
}
}
// Carga debug.yaml desde la carpeta del sistema (solo en _DEBUG)
void Director::loadDebugConfig() {
const std::string DEBUG_FILE = system_folder_ + "/debug.yaml";
std::ifstream file(DEBUG_FILE);
if (!file.good()) {
// Crear fichero por defecto
std::ofstream out(DEBUG_FILE);
if (out.is_open()) {
out << "# Coffee Crisis Arcade Edition - Debug Configuration\n";
out << "# This file is only read in DEBUG builds.\n";
out << "#\n";
out << "# initial_section: logo, intro, title, game, credits, instructions, hiscore\n";
out << "# initial_options: none, 1p, 2p, both\n";
out << "# initial_stage: 0-based stage index (only when section is game)\n";
out << "# show_render_info: show FPS/driver/preset overlay\n";
out << "# resource_loading: preload, lazy\n";
out << "\n";
out << "initial_section: game\n";
out << "initial_options: 1p\n";
out << "initial_stage: 0\n";
out << "show_render_info: true\n";
out << "resource_loading: preload\n";
out.close();
}
// Usar defaults de DebugConfig
} else {
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
try {
auto yaml = fkyaml::node::deserialize(content);
if (yaml.contains("initial_section")) {
try {
debug_config.initial_section = yaml["initial_section"].get_value<std::string>();
} catch (...) {}
}
if (yaml.contains("initial_options")) {
try {
debug_config.initial_options = yaml["initial_options"].get_value<std::string>();
} catch (...) {}
}
if (yaml.contains("initial_stage")) {
try {
debug_config.initial_stage = yaml["initial_stage"].get_value<int>();
} catch (...) {}
}
if (yaml.contains("show_render_info")) {
try {
debug_config.show_render_info = yaml["show_render_info"].get_value<bool>();
} catch (...) {}
}
if (yaml.contains("resource_loading")) {
try {
debug_config.resource_loading = yaml["resource_loading"].get_value<std::string>();
} catch (...) {}
}
} catch (...) {
std::cout << "Error parsing debug.yaml, using defaults" << '\n';
}
}
// Mapear strings a enums
const auto& sec = debug_config.initial_section;
if (sec == "logo") {
Section::name = Section::Name::LOGO;
} else if (sec == "intro") {
Section::name = Section::Name::INTRO;
} else if (sec == "title") {
Section::name = Section::Name::TITLE;
} else if (sec == "game") {
Section::name = Section::Name::GAME;
} else if (sec == "credits") {
Section::name = Section::Name::CREDITS;
} else if (sec == "instructions") {
Section::name = Section::Name::INSTRUCTIONS;
} else if (sec == "hiscore") {
Section::name = Section::Name::HI_SCORE_TABLE;
} else {
Section::name = Section::Name::GAME;
}
const auto& opt = debug_config.initial_options;
if (opt == "none") {
Section::options = Section::Options::NONE;
} else if (opt == "1p") {
Section::options = Section::Options::GAME_PLAY_1P;
} else if (opt == "2p") {
Section::options = Section::Options::GAME_PLAY_2P;
} else if (opt == "both") {
Section::options = Section::Options::GAME_PLAY_BOTH;
} else {
Section::options = Section::Options::GAME_PLAY_1P;
}
}
// Crea la carpeta del sistema donde guardar datos
void Director::createSystemFolder(const std::string& folder) {
auto result = SystemUtils::createApplicationFolder(folder, system_folder_);
if (result != SystemUtils::Result::SUCCESS) {
std::cerr << "Error creando carpeta del sistema: "
<< SystemUtils::resultToString(result) << '\n';
exit(EXIT_FAILURE);
}
}
// Libera todos los unique_ptr de sección (solo uno tiene propiedad a la vez)
void Director::resetActiveSection() {
logo_.reset();
intro_.reset();
title_.reset();
game_.reset();
instructions_.reset();
hi_score_table_.reset();
credits_.reset();
}
// Destruye la sección anterior y construye la nueva cuando Section::name cambia
void Director::handleSectionTransition() {
// RESET: recarga recursos y vuelve a LOGO (el propio reset() cambia Section::name)
if (Section::name == Section::Name::RESET) {
resetActiveSection(); // libera recursos actuales antes del reload
reset();
}
if (Section::name == last_built_section_name_) {
return; // ya tenemos la sección correcta viva
}
// Destruye la sección anterior
resetActiveSection();
// Construye la nueva
switch (Section::name) {
case Section::Name::LOGO:
logo_ = std::make_unique<Logo>();
break;
case Section::Name::INTRO:
intro_ = std::make_unique<Intro>();
break;
case Section::Name::TITLE:
title_ = std::make_unique<Title>();
break;
case Section::Name::GAME: {
Player::Id player_id = Player::Id::PLAYER1;
switch (Section::options) {
case Section::Options::GAME_PLAY_1P:
player_id = Player::Id::PLAYER1;
break;
case Section::Options::GAME_PLAY_2P:
player_id = Player::Id::PLAYER2;
break;
case Section::Options::GAME_PLAY_BOTH:
player_id = Player::Id::BOTH_PLAYERS;
break;
default:
break;
}
#ifdef _DEBUG
const int CURRENT_STAGE = debug_config.initial_stage;
#else
constexpr int CURRENT_STAGE = 0;
#endif
game_ = std::make_unique<Game>(player_id, CURRENT_STAGE, Game::DEMO_OFF);
break;
}
case Section::Name::GAME_DEMO: {
const auto PLAYER_ID = static_cast<Player::Id>((rand() % 2) + 1);
constexpr auto CURRENT_STAGE = 0;
game_ = std::make_unique<Game>(PLAYER_ID, CURRENT_STAGE, Game::DEMO_ON);
break;
}
case Section::Name::INSTRUCTIONS:
instructions_ = std::make_unique<Instructions>();
break;
case Section::Name::CREDITS:
credits_ = std::make_unique<Credits>();
break;
case Section::Name::HI_SCORE_TABLE:
hi_score_table_ = std::make_unique<HiScoreTable>();
break;
case Section::Name::RESET:
case Section::Name::QUIT:
default:
break;
}
last_built_section_name_ = Section::name;
}
// Reinicia objetos y vuelve a la sección inicial
void Director::reset() {
Options::saveToFile();
Options::loadFromFile();
Lang::setLanguage(Options::settings.language);
Audio::get()->stopMusic();
Audio::get()->stopAllSounds();
Resource::get()->reload();
ServiceMenu::get()->reset();
Section::name = Section::Name::LOGO;
}
// Avanza un frame de la sección activa (llamado desde SDL_AppIterate)
auto Director::iterate() -> SDL_AppResult {
if (Section::name == Section::Name::QUIT) {
return SDL_APP_SUCCESS;
}
// Gestiona las transiciones entre secciones (destruye la anterior y construye la nueva)
handleSectionTransition();
// Ejecuta un frame de la sección activa
if (logo_) {
logo_->iterate();
} else if (intro_) {
intro_->iterate();
} else if (title_) {
title_->iterate();
} else if (game_) {
game_->iterate();
} else if (instructions_) {
instructions_->iterate();
} else if (hi_score_table_) {
hi_score_table_->iterate();
} else if (credits_) {
credits_->iterate();
}
return (Section::name == Section::Name::QUIT) ? SDL_APP_SUCCESS : SDL_APP_CONTINUE;
}
// Procesa un evento SDL (llamado desde SDL_AppEvent)
auto Director::handleEvent(SDL_Event& event) -> SDL_AppResult {
// Eventos globales (SDL_EVENT_QUIT, resize, render target reset, hotplug, service menu, ratón)
GlobalEvents::handle(event);
// Reenvía a la sección activa
if (logo_) {
logo_->handleEvent(event);
} else if (intro_) {
intro_->handleEvent(event);
} else if (title_) {
title_->handleEvent(event);
} else if (game_) {
game_->handleEvent(event);
} else if (instructions_) {
instructions_->handleEvent(event);
} else if (hi_score_table_) {
hi_score_table_->handleEvent(event);
} else if (credits_) {
credits_->handleEvent(event);
}
return (Section::name == Section::Name::QUIT) ? SDL_APP_SUCCESS : SDL_APP_CONTINUE;
}
// Apaga el sistema de forma segura
void Director::shutdownSystem(bool should_shutdown) {
if (should_shutdown) {
auto result = SystemShutdown::shutdownSystem(5, true); // 5 segundos, forzar apps
if (result != SystemShutdown::ShutdownResult::SUCCESS) {
std::cerr << SystemShutdown::resultToString(result) << '\n';
}
}
}

View File

@@ -0,0 +1,87 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_AppResult, SDL_Event
#include <memory> // Para unique_ptr
#include <string> // Para string
#include "section.hpp" // Para Section::Name
namespace Lang {
enum class Code : int;
}
// Declaraciones adelantadas de las secciones
class Logo;
class Intro;
class Title;
class Game;
class Instructions;
class HiScoreTable;
class Credits;
// --- Clase Director: gestor principal de la aplicación ---
class Director {
public:
// --- Constructor y destructor ---
Director();
~Director();
// --- Callbacks para SDL_MAIN_USE_CALLBACKS ---
auto iterate() -> SDL_AppResult; // Avanza un frame de la sección activa
auto handleEvent(SDL_Event& event) -> SDL_AppResult; // Procesa un evento SDL
// --- Debug config (accesible desde otras clases) ---
struct DebugConfig {
std::string initial_section;
std::string initial_options;
int initial_stage = 0;
bool show_render_info = true;
std::string resource_loading;
DebugConfig()
: initial_section("game"),
initial_options("1p"),
resource_loading("preload") {}
};
static inline DebugConfig debug_config;
private:
// --- Variables internas ---
std::string executable_path_; // Ruta del ejecutable
std::string system_folder_; // Carpeta del sistema para almacenar datos
// --- Sección activa (una y sólo una viva en cada momento) ---
std::unique_ptr<Logo> logo_;
std::unique_ptr<Intro> intro_;
std::unique_ptr<Title> title_;
std::unique_ptr<Game> game_;
std::unique_ptr<Instructions> instructions_;
std::unique_ptr<HiScoreTable> hi_score_table_;
std::unique_ptr<Credits> credits_;
Section::Name last_built_section_name_ = Section::Name::RESET;
// --- Inicialización y cierre del sistema ---
void init(); // Inicializa la aplicación
static void close(); // Cierra y libera recursos
// --- Configuración inicial ---
static void loadParams(); // Carga los parámetros del programa
static void loadScoreFile(); // Carga el fichero de puntuaciones
void createSystemFolder(const std::string& folder); // Crea la carpeta del sistema
void loadDebugConfig(); // Carga debug.yaml (solo en _DEBUG)
// --- Gestión de entrada y archivos ---
void loadAssets(); // Crea el índice de archivos disponibles
// --- Gestión de secciones ---
void handleSectionTransition(); // Destruye la sección anterior y construye la nueva si Section::name ha cambiado
void resetActiveSection(); // Libera todos los unique_ptr de sección
static void reset(); // Reinicia objetos y vuelve a la sección inicial
// --- Gestión de archivos de idioma ---
auto getLangFile(Lang::Code code) -> std::string; // Obtiene un fichero de idioma según el código
// --- Apagado del sistema ---
static void shutdownSystem(bool should_shutdown); // Apaga el sistema
};

View File

@@ -0,0 +1,69 @@
#include "global_events.hpp"
#include <SDL3/SDL.h> // Para SDL_EventType, SDL_Event, SDL_LogInfo, SDL_LogCategory
#include <cstddef> // Para size_t
#include <iostream> // Para std::cout
#include <string> // Para allocator, operator+, string
#include <vector> // Para vector
#include "input.hpp" // Para Input
#include "lang.hpp" // Para getText
#include "mouse.hpp" // Para handleEvent
#include "options.hpp" // Para GamepadManager, gamepad_manager
#include "screen.hpp" // Para Screen
#include "section.hpp" // Para Name, Options, name, options
#include "ui/notifier.hpp" // Para Notifier
#include "ui/service_menu.hpp" // Para ServiceMenu
namespace GlobalEvents {
// Comprueba los eventos de Input y muestra notificaciones
void handleInputEvents(const SDL_Event& event) {
static auto* input_ = Input::get();
auto message = input_->handleEvent(event);
if (message.empty()) {
return;
}
// Reemplazo de palabras clave por texto localizado
size_t pos;
while ((pos = message.find(" CONNECTED")) != std::string::npos) {
message.replace(pos, std::string(" CONNECTED").length(), " " + Lang::getText("[NOTIFICATIONS] CONNECTED"));
}
while ((pos = message.find(" DISCONNECTED")) != std::string::npos) {
message.replace(pos, std::string(" DISCONNECTED").length(), " " + Lang::getText("[NOTIFICATIONS] DISCONNECTED"));
}
Options::gamepad_manager.assignAndLinkGamepads();
Options::gamepad_manager.resyncGamepadsWithPlayers();
Notifier::get()->show({message});
ServiceMenu::get()->refresh();
}
// Comprueba los eventos que se pueden producir en cualquier sección del juego
void handle(const SDL_Event& event) {
switch (event.type) {
case SDL_EVENT_QUIT: // Evento de salida de la aplicación
Section::name = Section::Name::QUIT;
Section::options = Section::Options::NONE;
return;
case SDL_EVENT_RENDER_DEVICE_RESET:
case SDL_EVENT_RENDER_TARGETS_RESET:
std::cout << "SDL_RENDER_TARGETS_RESET" << '\n';
break;
case SDL_EVENT_WINDOW_RESIZED:
Screen::initShaders();
break;
default:
break;
}
ServiceMenu::get()->handleEvent(event);
Mouse::handleEvent(event);
handleInputEvents(event);
}
} // namespace GlobalEvents

View File

@@ -0,0 +1,9 @@
#pragma once
#include <SDL3/SDL.h>
// --- Namespace GlobalEvents: maneja eventos globales del juego ---
namespace GlobalEvents {
// --- Funciones ---
void handle(const SDL_Event& event); // Comprueba los eventos que se pueden producir en cualquier sección del juego
} // namespace GlobalEvents

View File

@@ -0,0 +1,48 @@
#pragma once
/*
Namespace section: define los estados/secciones principales del programa,
así como las opciones y modos especiales (como el Attract Mode).
Proporciona variables globales para gestionar el flujo entre secciones.
*/
namespace Section {
// --- Enumeraciones de secciones del programa ---
enum class Name {
RESET, // Inicialización
LOGO, // Pantalla de logo
INTRO, // Introducción
TITLE, // Pantalla de título/menú principal
GAME, // Juego principal
HI_SCORE_TABLE, // Tabla de récords
GAME_DEMO, // Modo demo
INSTRUCTIONS, // Instrucciones
CREDITS, // Créditos
QUIT, // Salir del juego
};
// --- Opciones para la sección actual ---
enum class Options {
GAME_PLAY_1P, // Iniciar el juego con el jugador 1
GAME_PLAY_2P, // Iniciar el juego con el jugador 2
GAME_PLAY_BOTH, // Iniciar el juego con los dos jugadores
TITLE_TIME_OUT, // Timeout en el título
TITLE_1, // Opción 1 en el título
TITLE_2, // Opción 2 en el título
RELOAD, // Recargar sección
HI_SCORE_AFTER_PLAYING, // Mostrar récord tras jugar
SHUTDOWN, // Apagar el sistema
NONE, // Sin opción
};
// --- Modos para el Attract Mode ---
enum class AttractMode {
TITLE_TO_DEMO, // Pasar de título a demo
TITLE_TO_LOGO, // Pasar de título a logo
};
// --- Variables globales de estado ---
inline Name name = Name::RESET;
inline Options options = Options::NONE;
inline AttractMode attract_mode = AttractMode::TITLE_TO_DEMO;
} // namespace Section

View File

@@ -0,0 +1,157 @@
#include "shutdown.hpp"
#include <sys/types.h> // Para pid_t
#include <cstdlib> // Para WEXITSTATUS
#include <iostream> // Para char_traits, basic_ostream, operator<<, cerr
#include <vector> // Para vector
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/wait.h> // Para waitpid
#include <unistd.h> // Para _exit, execvp, fork
#endif
namespace SystemShutdown {
#ifndef _WIN32
// Función auxiliar para sistemas Unix-like
auto executeUnixShutdown(const char* command, const std::vector<char*>& args) -> ShutdownResult {
pid_t pid = fork();
if (pid == 0) {
// Proceso hijo
execvp(command, args.data());
// Si llegamos aquí, execvp falló
std::cerr << "Error: No se pudo ejecutar " << command << '\n';
_exit(1);
} else if (pid > 0) {
// Proceso padre
int status;
waitpid(pid, &status, 0);
return (WEXITSTATUS(status) == 0) ? ShutdownResult::SUCCESS : ShutdownResult::ERROR_SYSTEM_CALL;
} else {
return ShutdownResult::ERROR_FORK_FAILED;
}
}
#endif
// Implementación de las funciones públicas
auto shutdownSystem() -> ShutdownResult {
ShutdownConfig config;
return shutdownSystem(config);
}
auto shutdownSystem(int delay_seconds, bool force_apps) -> ShutdownResult {
ShutdownConfig config;
config.delay_seconds = delay_seconds;
config.force_close_apps = force_apps;
return shutdownSystem(config);
}
auto shutdownSystem(const ShutdownConfig& config) -> ShutdownResult {
#ifdef _WIN32
// Windows: Usar CreateProcess
STARTUPINFOA si = {0};
PROCESS_INFORMATION pi = {0};
si.cb = sizeof(si);
// Crear comando con el delay especificado
std::string command = "shutdown.exe /s /t " + std::to_string(config.delay_seconds);
if (config.force_close_apps) {
command += " /f";
}
// CreateProcess necesita un array de char modificable
char* cmd_buffer = new char[command.length() + 1];
strcpy(cmd_buffer, command.c_str());
bool success = CreateProcessA(
NULL, // lpApplicationName
cmd_buffer, // lpCommandLine
NULL, // lpProcessAttributes
NULL, // lpThreadAttributes
FALSE, // bInheritHandles
0, // dwCreationFlags
NULL, // lpEnvironment
NULL, // lpCurrentDirectory
&si, // lpStartupInfo
&pi // lpProcessInformation
);
delete[] cmd_buffer;
if (success) {
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return ShutdownResult::SUCCESS;
} else {
DWORD error = GetLastError();
if (error == ERROR_ACCESS_DENIED) {
return ShutdownResult::ERROR_PERMISSION;
}
return ShutdownResult::ERROR_SYSTEM_CALL;
}
#elif __APPLE__
// macOS - apagado inmediato
std::vector<char*> args = {
const_cast<char*>("shutdown"),
const_cast<char*>("-h"),
const_cast<char*>("now"),
nullptr};
return executeUnixShutdown("shutdown", args);
#elif __linux__
// Linux - apagado inmediato
std::vector<char*> args = {
const_cast<char*>("shutdown"),
const_cast<char*>("-h"),
const_cast<char*>("now"),
nullptr};
return executeUnixShutdown("shutdown", args);
#else
return ShutdownResult::ERROR_UNSUPPORTED;
#endif
}
auto resultToString(ShutdownResult result) -> const char* {
switch (result) {
case ShutdownResult::SUCCESS:
return "Apagado iniciado exitosamente";
case ShutdownResult::ERROR_PERMISSION:
return "Error: Permisos insuficientes";
case ShutdownResult::ERROR_SYSTEM_CALL:
return "Error: Fallo en la llamada al sistema";
case ShutdownResult::ERROR_FORK_FAILED:
return "Error: No se pudo crear proceso hijo";
case ShutdownResult::ERROR_UNSUPPORTED:
return "Error: Sistema operativo no soportado";
default:
return "Error desconocido";
}
}
auto isShutdownSupported() -> bool {
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
return true;
#else
return false;
#endif
}
auto getRequiredPermissions() -> const char* {
#ifdef _WIN32
return "Requiere permisos de Administrador en Windows";
#elif defined(__APPLE__) || defined(__linux__)
return "Requiere permisos de root/sudo en Unix";
#else
return "Sistema no soportado";
#endif
}
} // namespace SystemShutdown

View File

@@ -0,0 +1,33 @@
#pragma once
// --- Namespace SystemShutdown: utilidad multiplataforma para apagar el sistema de forma segura ---
namespace SystemShutdown {
// --- Enums ---
enum class ShutdownResult {
SUCCESS = 0, // Éxito
ERROR_PERMISSION, // Error de permisos insuficientes
ERROR_SYSTEM_CALL, // Error en la llamada al sistema
ERROR_FORK_FAILED, // Error al crear proceso hijo (Unix)
ERROR_UNSUPPORTED // Sistema operativo no soportado
};
// --- Estructuras ---
struct ShutdownConfig {
int delay_seconds{5}; // Segundos de retraso antes del apagado
bool force_close_apps{true}; // Forzar cierre de aplicaciones
const char* shutdown_message{"El sistema se apagará..."}; // Mensaje mostrado durante el apagado
// Constructor con valores por defecto
ShutdownConfig() = default;
};
// --- Funciones ---
auto shutdownSystem() -> ShutdownResult; // Apaga el sistema con configuración por defecto
auto shutdownSystem(const ShutdownConfig& config) -> ShutdownResult; // Apaga el sistema con configuración personalizada
auto shutdownSystem(int delay_seconds, bool force_apps = true) -> ShutdownResult; // Apaga el sistema con parámetros simples
auto resultToString(ShutdownResult result) -> const char*; // Convierte un código de resultado a string descriptivo
auto isShutdownSupported() -> bool; // Verifica si el sistema actual soporta apagado programático
auto getRequiredPermissions() -> const char*; // Obtiene información sobre los permisos necesarios
} // namespace SystemShutdown

View File

@@ -0,0 +1,19 @@
#pragma once
/**
* Interfaz para acceso a información de fases.
* Proporciona una API mínima para componentes que necesitan interactuar con datos de fases
* sin requerir acceso a toda la funcionalidad de StageManager.
*/
class IStageInfo {
public:
virtual ~IStageInfo() = default;
// Interfaz de recolección de poder
[[nodiscard]] virtual auto canCollectPower() const -> bool = 0;
virtual void enablePowerCollection() = 0;
virtual void addPower(int amount) = 0;
// Ajuste de comportamiento del gameplay
[[nodiscard]] virtual auto getCurrentMenaceLevel() const -> int = 0;
};

View File

@@ -0,0 +1,191 @@
#include "system_utils.hpp"
#include <sys/stat.h> // Para stat, mkdir, S_ISDIR
#include <cerrno> // Para EACCES, EEXIST, ENAMETOOLONG, errno
#include <cstdlib> // Para getenv, size_t
#ifdef _WIN32
#include <direct.h>
#include <shlobj.h>
#include <windows.h>
// Evitar conflictos con macros de Windows
#ifdef ERROR_ALREADY_EXISTS
#undef ERROR_ALREADY_EXISTS
#endif
#else
#include <pwd.h> // Para getpwuid, passwd
#include <unistd.h> // Para getuid
#endif
namespace SystemUtils {
// Función auxiliar para crear una carpeta individual
auto createSingleFolder(const std::string& path, int permissions) -> Result {
struct stat st = {.st_dev = 0};
// Verificar si ya existe
if (stat(path.c_str(), &st) == 0) {
return Result::SUCCESS; // Ya existe, no es error por defecto
}
// Intentar crear la carpeta
int result;
#ifdef _WIN32
result = _mkdir(path.c_str());
#else
result = mkdir(path.c_str(), permissions);
#endif
if (result == -1) {
switch (errno) {
case EACCES:
return Result::PERMISSION_DENIED;
case EEXIST:
return Result::ALREADY_EXISTS;
case ENAMETOOLONG:
return Result::PATH_TOO_LONG;
default:
return Result::UNKNOWN_ERROR;
}
}
return Result::SUCCESS;
}
// Función auxiliar para crear carpetas padre recursivamente
auto createParentFolders(const std::string& path, int permissions) -> Result {
size_t pos = 0;
while ((pos = path.find('/', pos + 1)) != std::string::npos) {
std::string parent = path.substr(0, pos);
if (!parent.empty() && !folderExists(parent)) {
Result result = createSingleFolder(parent, permissions);
if (result != Result::SUCCESS && result != Result::ALREADY_EXISTS) {
return result;
}
}
}
return Result::SUCCESS;
}
auto createApplicationFolder(const std::string& app_name, std::string& out_path) -> Result {
FolderConfig config;
return createApplicationFolder(app_name, out_path, config);
}
auto createApplicationFolder(const std::string& app_name, std::string& out_path, const FolderConfig& config) -> Result {
out_path = getApplicationDataPath(app_name);
return createFolder(out_path, config);
}
auto createFolder(const std::string& path) -> Result {
FolderConfig config;
return createFolder(path, config);
}
auto createFolder(const std::string& path, const FolderConfig& config) -> Result {
if (path.empty()) {
return Result::INVALID_PATH;
}
// Verificar si ya existe y si eso es un error
if (folderExists(path) && config.fail_if_exists) {
return Result::ALREADY_EXISTS;
}
// Crear carpetas padre si es necesario
if (config.create_parents) {
Result parent_result = createParentFolders(path, config.permissions);
if (parent_result != Result::SUCCESS) {
return parent_result;
}
}
// Crear la carpeta final
return createSingleFolder(path, config.permissions);
}
auto getApplicationDataPath(const std::string& app_name) -> std::string {
#ifdef _WIN32
char* appdata = getenv("APPDATA");
if (appdata) {
return std::string(appdata) + "/" + app_name;
}
return "C:/Users/Default/AppData/Roaming/" + app_name;
#elif __APPLE__
std::string home = getHomeDirectory();
return home + "/Library/Application Support/" + app_name;
#elif __linux__
std::string home = getHomeDirectory();
return home + "/.config/" + app_name;
#else
// Fallback genérico
std::string home = getHomeDirectory();
return home + "/." + app_name;
#endif
}
auto folderExists(const std::string& path) -> bool {
struct stat st = {.st_dev = 0};
return (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode));
}
auto resultToString(Result result) -> const char* {
switch (result) {
case Result::SUCCESS:
return "Operación exitosa";
case Result::PERMISSION_DENIED:
return "Error: Permisos insuficientes";
case Result::PATH_TOO_LONG:
return "Error: Ruta demasiado larga";
case Result::ALREADY_EXISTS:
return "Error: La carpeta ya existe";
case Result::INVALID_PATH:
return "Error: Ruta inválida";
case Result::UNKNOWN_ERROR:
return "Error desconocido";
default:
return "Error no identificado";
}
}
auto getHomeDirectory() -> std::string {
#ifdef _WIN32
char* userprofile = getenv("USERPROFILE");
if (userprofile) {
return std::string(userprofile);
}
return "C:/Users/Default";
#else
struct passwd* pw = getpwuid(getuid());
if ((pw != nullptr) && (pw->pw_dir != nullptr)) {
return {pw->pw_dir};
}
// Fallback
char* home = getenv("HOME");
if (home != nullptr) {
return {home};
}
return "/tmp";
#endif
}
auto getTempDirectory() -> std::string {
#ifdef _WIN32
char* temp = getenv("TEMP");
if (temp) {
return std::string(temp);
}
return "C:/Windows/Temp";
#else
return "/tmp";
#endif
}
} // namespace SystemUtils

View File

@@ -0,0 +1,38 @@
#pragma once
#include <string>
// --- Namespace SystemUtils: utilidades multiplataforma para operaciones del sistema ---
namespace SystemUtils {
// --- Enums ---
enum class Result { // Códigos de resultado para operaciones del sistema
SUCCESS = 0,
PERMISSION_DENIED, // Sin permisos para crear la carpeta
PATH_TOO_LONG, // Ruta demasiado larga
ALREADY_EXISTS, // Ya existe (solo si se considera error)
INVALID_PATH, // Ruta inválida
UNKNOWN_ERROR // Error desconocido
};
// --- Estructuras ---
struct FolderConfig { // Configuración para creación de carpetas
bool create_parents{true}; // Crear carpetas padre si no existen
bool fail_if_exists{false}; // Fallar si la carpeta ya existe
int permissions{0755}; // Permisos Unix (ignorado en Windows)
// Constructor con valores por defecto
FolderConfig() = default;
};
// --- Funciones ---
auto createApplicationFolder(const std::string& app_name, std::string& out_path) -> Result; // Crea la carpeta del sistema donde guardar datos de la aplicación
auto createApplicationFolder(const std::string& app_name, std::string& out_path, const FolderConfig& config) -> Result; // Crea la carpeta del sistema con configuración personalizada
auto createFolder(const std::string& path) -> Result; // Crea una carpeta en la ruta especificada
auto createFolder(const std::string& path, const FolderConfig& config) -> Result; // Crea una carpeta con configuración personalizada
auto getApplicationDataPath(const std::string& app_name) -> std::string; // Obtiene la ruta de datos de la aplicación (sin crearla)
auto folderExists(const std::string& path) -> bool; // Verifica si una carpeta existe
auto resultToString(Result result) -> const char*; // Convierte un código de resultado a string descriptivo
auto getHomeDirectory() -> std::string; // Obtiene el directorio home del usuario
auto getTempDirectory() -> std::string; // Obtiene el directorio temporal del sistema
} // namespace SystemUtils