reestructuració
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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(¤t_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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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},
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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_);
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
@@ -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_; }
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
@@ -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_;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user