segon commit
This commit is contained in:
195
source/core/audio/audio.cpp
Normal file
195
source/core/audio/audio.cpp
Normal file
@@ -0,0 +1,195 @@
|
||||
#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"
|
||||
// clang-format on
|
||||
|
||||
#include "core/audio/jail_audio.hpp" // Para JA_FadeOutMusic, JA_Init, JA_PauseM...
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "game/options.hpp" // Para AudioOptions, audio, MusicOptions
|
||||
|
||||
// Singleton
|
||||
Audio* Audio::instance = nullptr;
|
||||
|
||||
// Inicializa la instancia única del singleton
|
||||
void Audio::init() { Audio::instance = new Audio(); }
|
||||
|
||||
// Libera la instancia
|
||||
void Audio::destroy() { delete Audio::instance; }
|
||||
|
||||
// Obtiene la instancia
|
||||
auto Audio::get() -> Audio* { return Audio::instance; }
|
||||
|
||||
// Constructor
|
||||
Audio::Audio() { initSDLAudio(); }
|
||||
|
||||
// Destructor
|
||||
Audio::~Audio() {
|
||||
JA_Quit();
|
||||
}
|
||||
|
||||
// Método principal
|
||||
void Audio::update() {
|
||||
JA_Update();
|
||||
}
|
||||
|
||||
// Reproduce la música
|
||||
void Audio::playMusic(const std::string& name, const int loop) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
bool new_loop = (loop != 0);
|
||||
|
||||
// Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada
|
||||
if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Intentar obtener recurso; si falla, no tocar estado
|
||||
auto* resource = Resource::Cache::get()->getMusic(name);
|
||||
if (resource == nullptr) {
|
||||
// manejo de error opcional
|
||||
return;
|
||||
}
|
||||
|
||||
// Si hay algo reproduciéndose, detenerlo primero (si el backend lo requiere)
|
||||
if (music_.state == MusicState::PLAYING) {
|
||||
JA_StopMusic(); // sustituir por la función de stop real del API si tiene otro nombre
|
||||
}
|
||||
|
||||
// Llamada al motor para reproducir la nueva pista
|
||||
JA_PlayMusic(resource, loop);
|
||||
|
||||
// Actualizar estado y metadatos después de iniciar con éxito
|
||||
music_.name = name;
|
||||
music_.loop = new_loop;
|
||||
music_.state = MusicState::PLAYING;
|
||||
}
|
||||
|
||||
// Pausa la música
|
||||
void Audio::pauseMusic() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (music_enabled_ && music_.state == MusicState::PLAYING) {
|
||||
JA_PauseMusic();
|
||||
music_.state = MusicState::PAUSED;
|
||||
}
|
||||
}
|
||||
|
||||
// Continua la música pausada
|
||||
void Audio::resumeMusic() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (music_enabled_ && music_.state == MusicState::PAUSED) {
|
||||
JA_ResumeMusic();
|
||||
music_.state = MusicState::PLAYING;
|
||||
}
|
||||
}
|
||||
|
||||
// Detiene la música
|
||||
void Audio::stopMusic() { // NOLINT(readability-make-member-function-const)
|
||||
if (music_enabled_) {
|
||||
JA_StopMusic();
|
||||
music_.state = MusicState::STOPPED;
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduce un sonido por nombre
|
||||
void Audio::playSound(const std::string& name, Group group) const {
|
||||
if (sound_enabled_) {
|
||||
JA_PlaySound(Resource::Cache::get()->getSound(name), 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduce un sonido por puntero directo
|
||||
void Audio::playSound(JA_Sound_t* sound, Group group) const {
|
||||
if (sound_enabled_) {
|
||||
JA_PlaySound(sound, 0, static_cast<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(float sound_volume, Group group) const {
|
||||
if (sound_enabled_) {
|
||||
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
const float CONVERTED_VOLUME = sound_volume * Options::audio.volume;
|
||||
JA_SetSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Establece el volumen de la música
|
||||
void Audio::setMusicVolume(float music_volume) const {
|
||||
if (music_enabled_) {
|
||||
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
const float CONVERTED_VOLUME = music_volume * Options::audio.volume;
|
||||
JA_SetMusicVolume(CONVERTED_VOLUME);
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica la configuración
|
||||
void Audio::applySettings() {
|
||||
enable(Options::audio.enabled);
|
||||
}
|
||||
|
||||
// Establecer estado general
|
||||
void Audio::enable(bool value) {
|
||||
enabled_ = value;
|
||||
|
||||
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME);
|
||||
setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME);
|
||||
}
|
||||
|
||||
// Inicializa SDL Audio
|
||||
void Audio::initSDLAudio() {
|
||||
if (!SDL_Init(SDL_INIT_AUDIO)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AUDIO could not initialize! SDL Error: %s", SDL_GetError());
|
||||
} else {
|
||||
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
|
||||
enable(Options::audio.enabled);
|
||||
|
||||
// Aplicar estado de música y sonido guardado en las opciones.
|
||||
// enable() ya aplica los volúmenes, pero no toca music_enabled_/sound_enabled_.
|
||||
// Si alguno está desactivado, hay que forzar el volumen a 0 en el backend.
|
||||
if (!Options::audio.music.enabled) {
|
||||
setMusicVolume(0.0F); // music_enabled_=true aún → llega a JA
|
||||
enableMusic(false);
|
||||
}
|
||||
if (!Options::audio.sound.enabled) {
|
||||
setSoundVolume(0.0F); // sound_enabled_=true aún → llega a JA
|
||||
enableSound(false);
|
||||
}
|
||||
|
||||
std::cout << "\n** AUDIO SYSTEM **\n";
|
||||
std::cout << "Audio system initialized successfully\n";
|
||||
}
|
||||
}
|
||||
97
source/core/audio/audio.hpp
Normal file
97
source/core/audio/audio.hpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#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 float MAX_VOLUME = 1.0F; // Volumen máximo
|
||||
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo
|
||||
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
|
||||
|
||||
// --- Singleton ---
|
||||
static void init(); // Inicializa el objeto Audio
|
||||
static void destroy(); // Libera el objeto Audio
|
||||
static auto get() -> Audio*; // Obtiene el puntero al objeto Audio
|
||||
Audio(const Audio&) = delete; // Evitar copia
|
||||
auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación
|
||||
|
||||
static void update(); // Actualización del sistema de audio
|
||||
|
||||
// --- Control de música ---
|
||||
void playMusic(const std::string& name, int loop = -1); // Reproducir música en bucle
|
||||
void pauseMusic(); // Pausar reproducción de música
|
||||
void resumeMusic(); // Continua la música pausada
|
||||
void stopMusic(); // Detener completamente la música
|
||||
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
|
||||
|
||||
// --- Control de sonidos ---
|
||||
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
|
||||
void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
|
||||
void stopAllSounds() const; // Detener todos los sonidos
|
||||
|
||||
// --- Control de volumen ---
|
||||
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
|
||||
void setMusicVolume(float volume) const; // Ajustar volumen de música
|
||||
|
||||
// --- Configuración general ---
|
||||
void enable(bool value); // Establecer estado general
|
||||
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
|
||||
void applySettings(); // Aplica la configuración
|
||||
|
||||
// --- Configuración de sonidos ---
|
||||
void enableSound() { sound_enabled_ = true; } // Habilitar sonidos
|
||||
void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos
|
||||
void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos
|
||||
void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos
|
||||
|
||||
// --- Configuración de música ---
|
||||
void enableMusic() { music_enabled_ = true; } // Habilitar música
|
||||
void disableMusic() { music_enabled_ = false; } // Deshabilitar música
|
||||
void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música
|
||||
void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música
|
||||
|
||||
// --- Consultas de estado ---
|
||||
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
|
||||
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
|
||||
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
|
||||
[[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; }
|
||||
[[nodiscard]] static auto getRealMusicState() -> MusicState;
|
||||
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
|
||||
|
||||
private:
|
||||
// --- Tipos anidados ---
|
||||
struct Music {
|
||||
MusicState state{MusicState::STOPPED}; // Estado actual de la música
|
||||
std::string name; // Última pista de música reproducida
|
||||
bool loop{false}; // Indica si se reproduce en bucle
|
||||
};
|
||||
|
||||
// --- Métodos ---
|
||||
Audio(); // Constructor privado
|
||||
~Audio(); // Destructor privado
|
||||
void initSDLAudio(); // Inicializa SDL Audio
|
||||
|
||||
// --- Variables miembro ---
|
||||
static Audio* instance; // Instancia única de Audio
|
||||
|
||||
Music music_; // Estado de la música
|
||||
bool enabled_{true}; // Estado general del audio
|
||||
bool sound_enabled_{true}; // Estado de los efectos de sonido
|
||||
bool music_enabled_{true}; // Estado de la música
|
||||
};
|
||||
482
source/core/audio/jail_audio.hpp
Normal file
482
source/core/audio/jail_audio.hpp
Normal file
@@ -0,0 +1,482 @@
|
||||
#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
|
||||
|
||||
#define STB_VORBIS_HEADER_ONLY
|
||||
#include "external/stb_vorbis.h" // Para stb_vorbis_decode_memory
|
||||
|
||||
// --- Public Enums ---
|
||||
enum JA_Channel_state { JA_CHANNEL_INVALID,
|
||||
JA_CHANNEL_FREE,
|
||||
JA_CHANNEL_PLAYING,
|
||||
JA_CHANNEL_PAUSED,
|
||||
JA_SOUND_DISABLED };
|
||||
enum JA_Music_state { JA_MUSIC_INVALID,
|
||||
JA_MUSIC_PLAYING,
|
||||
JA_MUSIC_PAUSED,
|
||||
JA_MUSIC_STOPPED,
|
||||
JA_MUSIC_DISABLED };
|
||||
|
||||
// --- Struct Definitions ---
|
||||
#define JA_MAX_SIMULTANEOUS_CHANNELS 20
|
||||
#define JA_MAX_GROUPS 2
|
||||
|
||||
struct JA_Sound_t {
|
||||
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||
Uint32 length{0};
|
||||
Uint8* buffer{NULL};
|
||||
};
|
||||
|
||||
struct JA_Channel_t {
|
||||
JA_Sound_t* sound{nullptr};
|
||||
int pos{0};
|
||||
int times{0};
|
||||
int group{0};
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
JA_Channel_state state{JA_CHANNEL_FREE};
|
||||
};
|
||||
|
||||
struct JA_Music_t {
|
||||
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||
Uint32 length{0};
|
||||
Uint8* buffer{nullptr};
|
||||
char* filename{nullptr};
|
||||
|
||||
int pos{0};
|
||||
int times{0};
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
JA_Music_state state{JA_MUSIC_INVALID};
|
||||
};
|
||||
|
||||
// --- Internal Global State ---
|
||||
// Marcado 'inline' (C++17) para asegurar una única instancia.
|
||||
|
||||
inline JA_Music_t* current_music{nullptr};
|
||||
inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
|
||||
|
||||
inline SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000};
|
||||
inline float JA_musicVolume{1.0f};
|
||||
inline float JA_soundVolume[JA_MAX_GROUPS];
|
||||
inline bool JA_musicEnabled{true};
|
||||
inline bool JA_soundEnabled{true};
|
||||
inline SDL_AudioDeviceID sdlAudioDevice{0};
|
||||
|
||||
inline bool fading{false};
|
||||
inline int fade_start_time{0};
|
||||
inline int fade_duration{0};
|
||||
inline float fade_initial_volume{0.0f}; // Corregido de 'int' a 'float'
|
||||
|
||||
// --- Forward Declarations ---
|
||||
inline void JA_StopMusic();
|
||||
inline void JA_StopChannel(const int channel);
|
||||
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0);
|
||||
|
||||
// --- Core Functions ---
|
||||
|
||||
inline void JA_Update() {
|
||||
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
|
||||
if (fading) {
|
||||
int time = SDL_GetTicks();
|
||||
if (time > (fade_start_time + fade_duration)) {
|
||||
fading = false;
|
||||
JA_StopMusic();
|
||||
return;
|
||||
} else {
|
||||
const int time_passed = time - fade_start_time;
|
||||
const float percent = (float)time_passed / (float)fade_duration;
|
||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0 - percent));
|
||||
}
|
||||
}
|
||||
|
||||
if (current_music->times != 0) {
|
||||
if ((Uint32)SDL_GetAudioStreamAvailable(current_music->stream) < (current_music->length / 2)) {
|
||||
SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length);
|
||||
}
|
||||
if (current_music->times > 0) current_music->times--;
|
||||
} else {
|
||||
if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic();
|
||||
}
|
||||
}
|
||||
|
||||
if (JA_soundEnabled) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
|
||||
if (channels[i].state == JA_CHANNEL_PLAYING) {
|
||||
if (channels[i].times != 0) {
|
||||
if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) {
|
||||
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length);
|
||||
if (channels[i].times > 0) channels[i].times--;
|
||||
}
|
||||
} else {
|
||||
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
|
||||
#ifdef _DEBUG
|
||||
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
|
||||
#endif
|
||||
|
||||
JA_audioSpec = {format, num_channels, freq};
|
||||
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice
|
||||
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
|
||||
if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!");
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
|
||||
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f;
|
||||
}
|
||||
|
||||
inline void JA_Quit() {
|
||||
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice
|
||||
sdlAudioDevice = 0;
|
||||
}
|
||||
|
||||
// --- Music Functions ---
|
||||
|
||||
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
|
||||
JA_Music_t* music = new JA_Music_t();
|
||||
|
||||
int chan, samplerate;
|
||||
short* output;
|
||||
music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2;
|
||||
|
||||
music->spec.channels = chan;
|
||||
music->spec.freq = samplerate;
|
||||
music->spec.format = SDL_AUDIO_S16;
|
||||
music->buffer = static_cast<Uint8*>(SDL_malloc(music->length));
|
||||
SDL_memcpy(music->buffer, output, music->length);
|
||||
free(output);
|
||||
music->pos = 0;
|
||||
music->state = JA_MUSIC_STOPPED;
|
||||
|
||||
return music;
|
||||
}
|
||||
|
||||
inline JA_Music_t* JA_LoadMusic(const char* filename) {
|
||||
// [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid.
|
||||
FILE* f = fopen(filename, "rb");
|
||||
if (!f) return NULL; // Añadida comprobación de apertura
|
||||
fseek(f, 0, SEEK_END);
|
||||
long fsize = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
auto* buffer = static_cast<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) return; // Añadida comprobación de music
|
||||
|
||||
JA_StopMusic();
|
||||
|
||||
current_music = music;
|
||||
current_music->pos = 0;
|
||||
current_music->state = JA_MUSIC_PLAYING;
|
||||
current_music->times = loop;
|
||||
|
||||
current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &JA_audioSpec);
|
||||
if (!current_music->stream) { // Comprobar creación de stream
|
||||
SDL_Log("Failed to create audio stream!");
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
return;
|
||||
}
|
||||
if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n");
|
||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
||||
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
|
||||
}
|
||||
|
||||
inline char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) {
|
||||
if (!music) music = current_music;
|
||||
if (!music) return nullptr; // Añadida comprobación
|
||||
return music->filename;
|
||||
}
|
||||
|
||||
inline void JA_PauseMusic() {
|
||||
if (!JA_musicEnabled) return;
|
||||
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; // Comprobación mejorada
|
||||
|
||||
current_music->state = JA_MUSIC_PAUSED;
|
||||
SDL_UnbindAudioStream(current_music->stream);
|
||||
}
|
||||
|
||||
inline void JA_ResumeMusic() {
|
||||
if (!JA_musicEnabled) return;
|
||||
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; // Comprobación mejorada
|
||||
|
||||
current_music->state = JA_MUSIC_PLAYING;
|
||||
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
|
||||
}
|
||||
|
||||
inline void JA_StopMusic() {
|
||||
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
|
||||
|
||||
current_music->pos = 0;
|
||||
current_music->state = JA_MUSIC_STOPPED;
|
||||
if (current_music->stream) {
|
||||
SDL_DestroyAudioStream(current_music->stream);
|
||||
current_music->stream = nullptr;
|
||||
}
|
||||
// No liberamos filename aquí, se debería liberar en JA_DeleteMusic
|
||||
}
|
||||
|
||||
inline void JA_FadeOutMusic(const int milliseconds) {
|
||||
if (!JA_musicEnabled) return;
|
||||
if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return;
|
||||
|
||||
fading = true;
|
||||
fade_start_time = SDL_GetTicks();
|
||||
fade_duration = milliseconds;
|
||||
fade_initial_volume = JA_musicVolume;
|
||||
}
|
||||
|
||||
inline JA_Music_state JA_GetMusicState() {
|
||||
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
|
||||
if (!current_music) return JA_MUSIC_INVALID;
|
||||
|
||||
return current_music->state;
|
||||
}
|
||||
|
||||
inline void JA_DeleteMusic(JA_Music_t* music) {
|
||||
if (!music) return;
|
||||
if (current_music == music) {
|
||||
JA_StopMusic();
|
||||
current_music = nullptr;
|
||||
}
|
||||
SDL_free(music->buffer);
|
||||
if (music->stream) SDL_DestroyAudioStream(music->stream);
|
||||
free(music->filename); // filename se libera aquí
|
||||
delete music;
|
||||
}
|
||||
|
||||
inline float JA_SetMusicVolume(float volume) {
|
||||
JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f);
|
||||
if (current_music && current_music->stream) {
|
||||
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
|
||||
}
|
||||
return JA_musicVolume;
|
||||
}
|
||||
|
||||
inline void JA_SetMusicPosition(float value) {
|
||||
if (!current_music) return;
|
||||
current_music->pos = value * current_music->spec.freq;
|
||||
// Nota: Esta implementación de 'pos' no parece usarse en JA_Update para
|
||||
// el streaming. El streaming siempre parece empezar desde el principio.
|
||||
}
|
||||
|
||||
inline float JA_GetMusicPosition() {
|
||||
if (!current_music) return 0;
|
||||
return float(current_music->pos) / float(current_music->spec.freq);
|
||||
// Nota: Ver `JA_SetMusicPosition`
|
||||
}
|
||||
|
||||
inline void JA_EnableMusic(const bool value) {
|
||||
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
|
||||
|
||||
JA_musicEnabled = value;
|
||||
}
|
||||
|
||||
// --- Sound Functions ---
|
||||
|
||||
inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
sound->buffer = buffer;
|
||||
sound->length = length;
|
||||
// Nota: spec se queda con los valores por defecto.
|
||||
return sound;
|
||||
}
|
||||
|
||||
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) {
|
||||
SDL_Log("Failed to load WAV from memory: %s", SDL_GetError());
|
||||
delete sound;
|
||||
return nullptr;
|
||||
}
|
||||
return sound;
|
||||
}
|
||||
|
||||
inline JA_Sound_t* JA_LoadSound(const char* filename) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) {
|
||||
SDL_Log("Failed to load WAV file: %s", SDL_GetError());
|
||||
delete sound;
|
||||
return nullptr;
|
||||
}
|
||||
return sound;
|
||||
}
|
||||
|
||||
inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
|
||||
if (!JA_soundEnabled || !sound) return -1;
|
||||
|
||||
int channel = 0;
|
||||
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
|
||||
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
// No hay canal libre, reemplazamos el primero
|
||||
channel = 0;
|
||||
}
|
||||
|
||||
return JA_PlaySoundOnChannel(sound, channel, loop, group);
|
||||
}
|
||||
|
||||
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) {
|
||||
if (!JA_soundEnabled || !sound) return -1;
|
||||
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
|
||||
|
||||
JA_StopChannel(channel); // Detiene y limpia el canal si estaba en uso
|
||||
|
||||
channels[channel].sound = sound;
|
||||
channels[channel].times = loop;
|
||||
channels[channel].pos = 0;
|
||||
channels[channel].group = group; // Asignar grupo
|
||||
channels[channel].state = JA_CHANNEL_PLAYING;
|
||||
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
|
||||
|
||||
if (!channels[channel].stream) {
|
||||
SDL_Log("Failed to create audio stream for sound!");
|
||||
channels[channel].state = JA_CHANNEL_FREE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
|
||||
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
|
||||
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
inline void JA_DeleteSound(JA_Sound_t* sound) {
|
||||
if (!sound) return;
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||
if (channels[i].sound == sound) JA_StopChannel(i);
|
||||
}
|
||||
SDL_free(sound->buffer);
|
||||
delete sound;
|
||||
}
|
||||
|
||||
inline void JA_PauseChannel(const int channel) {
|
||||
if (!JA_soundEnabled) return;
|
||||
|
||||
if (channel == -1) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
|
||||
if (channels[i].state == JA_CHANNEL_PLAYING) {
|
||||
channels[i].state = JA_CHANNEL_PAUSED;
|
||||
SDL_UnbindAudioStream(channels[i].stream);
|
||||
}
|
||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
if (channels[channel].state == JA_CHANNEL_PLAYING) {
|
||||
channels[channel].state = JA_CHANNEL_PAUSED;
|
||||
SDL_UnbindAudioStream(channels[channel].stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void JA_ResumeChannel(const int channel) {
|
||||
if (!JA_soundEnabled) return;
|
||||
|
||||
if (channel == -1) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
|
||||
if (channels[i].state == JA_CHANNEL_PAUSED) {
|
||||
channels[i].state = JA_CHANNEL_PLAYING;
|
||||
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
|
||||
}
|
||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
if (channels[channel].state == JA_CHANNEL_PAUSED) {
|
||||
channels[channel].state = JA_CHANNEL_PLAYING;
|
||||
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void JA_StopChannel(const int channel) {
|
||||
if (channel == -1) {
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||
if (channels[i].state != JA_CHANNEL_FREE) {
|
||||
if (channels[i].stream) SDL_DestroyAudioStream(channels[i].stream);
|
||||
channels[i].stream = nullptr;
|
||||
channels[i].state = JA_CHANNEL_FREE;
|
||||
channels[i].pos = 0;
|
||||
channels[i].sound = NULL;
|
||||
}
|
||||
}
|
||||
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
|
||||
if (channels[channel].state != JA_CHANNEL_FREE) {
|
||||
if (channels[channel].stream) SDL_DestroyAudioStream(channels[channel].stream);
|
||||
channels[channel].stream = nullptr;
|
||||
channels[channel].state = JA_CHANNEL_FREE;
|
||||
channels[channel].pos = 0;
|
||||
channels[channel].sound = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline JA_Channel_state JA_GetChannelState(const int channel) {
|
||||
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
|
||||
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
|
||||
|
||||
return channels[channel].state;
|
||||
}
|
||||
|
||||
inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para todos los grupos
|
||||
{
|
||||
const float v = SDL_clamp(volume, 0.0f, 1.0f);
|
||||
|
||||
if (group == -1) {
|
||||
for (int i = 0; i < JA_MAX_GROUPS; ++i) {
|
||||
JA_soundVolume[i] = v;
|
||||
}
|
||||
} else if (group >= 0 && group < JA_MAX_GROUPS) {
|
||||
JA_soundVolume[group] = v;
|
||||
} else {
|
||||
return v; // Grupo inválido
|
||||
}
|
||||
|
||||
// Aplicar volumen a canales activos
|
||||
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
|
||||
if (group == -1 || channels[i].group == group) {
|
||||
if (channels[i].stream) {
|
||||
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
inline void JA_EnableSound(const bool value) {
|
||||
if (!value) {
|
||||
JA_StopChannel(-1); // Detener todos los canales
|
||||
}
|
||||
JA_soundEnabled = value;
|
||||
}
|
||||
|
||||
inline float JA_SetVolume(float volume) {
|
||||
float v = JA_SetMusicVolume(volume);
|
||||
JA_SetSoundVolume(v, -1); // Aplicar a todos los grupos de sonido
|
||||
return v;
|
||||
}
|
||||
310
source/core/input/global_inputs.cpp
Normal file
310
source/core/input/global_inputs.cpp
Normal file
@@ -0,0 +1,310 @@
|
||||
#include "core/input/global_inputs.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string> // Para allocator, operator+, char_traits, string
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/input/input.hpp" // Para Input, InputAction, Input::DO_NOT_ALLOW_REPEAT
|
||||
#include "core/locale/locale.hpp" // Para Locale
|
||||
#include "core/rendering/render_info.hpp" // Para RenderInfo
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "game/options.hpp" // Para Options, options, OptionsVideo, Section
|
||||
#include "game/scene_manager.hpp" // Para SceneManager
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
#include "game/ui/notifier.hpp" // Para Notifier, NotificationText
|
||||
#include "utils/utils.hpp" // Para stringInVector
|
||||
|
||||
namespace GlobalInputs {
|
||||
|
||||
// Funciones internas
|
||||
namespace {
|
||||
void handleQuit() {
|
||||
// En la escena GAME el comportamiento es siempre el mismo (con o sin modo kiosko)
|
||||
if (SceneManager::current == SceneManager::Scene::GAME) {
|
||||
const std::string CODE = "PRESS AGAIN TO RETURN TO MENU";
|
||||
if (stringInVector(Notifier::get()->getCodes(), CODE)) {
|
||||
SceneManager::current = SceneManager::Scene::TITLE;
|
||||
} else {
|
||||
Notifier::get()->show({Locale::get()->get("ui.press_again_menu")}, Notifier::Style::DEFAULT, -1, true, CODE); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// En modo kiosko, fuera de GAME: mostrar el texto del kiosko y no salir nunca
|
||||
if (Options::kiosk.enabled) {
|
||||
const std::string KIOSK_CODE = "KIOSK_EXIT";
|
||||
if (!stringInVector(Notifier::get()->getCodes(), KIOSK_CODE)) {
|
||||
Notifier::get()->show({Options::kiosk.text}, Notifier::Style::DEFAULT, -1, true, KIOSK_CODE);
|
||||
}
|
||||
// Segunda pulsación: notificación ya activa → no hacer nada
|
||||
return;
|
||||
}
|
||||
|
||||
// Comportamiento normal fuera del modo kiosko
|
||||
const std::string CODE = "PRESS AGAIN TO EXIT";
|
||||
if (stringInVector(Notifier::get()->getCodes(), CODE)) {
|
||||
SceneManager::current = SceneManager::Scene::QUIT;
|
||||
} else {
|
||||
Notifier::get()->show({Locale::get()->get("ui.press_again_exit")}, Notifier::Style::DEFAULT, -1, true, CODE); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
}
|
||||
|
||||
void handleSkipSection() {
|
||||
switch (SceneManager::current) {
|
||||
case SceneManager::Scene::LOGO:
|
||||
case SceneManager::Scene::LOADING_SCREEN:
|
||||
case SceneManager::Scene::CREDITS:
|
||||
case SceneManager::Scene::DEMO:
|
||||
case SceneManager::Scene::GAME_OVER:
|
||||
case SceneManager::Scene::ENDING:
|
||||
case SceneManager::Scene::ENDING2:
|
||||
SceneManager::current = SceneManager::Scene::TITLE;
|
||||
SceneManager::options = SceneManager::Options::NONE;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void handleToggleBorder() {
|
||||
Screen::get()->toggleBorder();
|
||||
Notifier::get()->show({Locale::get()->get(Options::video.border.enabled ? "ui.border_enabled" : "ui.border_disabled")}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handleToggleVideoMode() {
|
||||
Screen::get()->toggleVideoMode();
|
||||
Notifier::get()->show({Locale::get()->get(static_cast<int>(Options::video.fullscreen) == 0 ? "ui.fullscreen_disabled" : "ui.fullscreen_enabled")}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handleDecWindowZoom() {
|
||||
if (Screen::get()->decWindowZoom()) {
|
||||
Notifier::get()->show({Locale::get()->get("ui.window_zoom") + std::to_string(Options::window.zoom)}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
}
|
||||
|
||||
void handleIncWindowZoom() {
|
||||
if (Screen::get()->incWindowZoom()) {
|
||||
Notifier::get()->show({Locale::get()->get("ui.window_zoom") + std::to_string(Options::window.zoom)}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
}
|
||||
|
||||
void handleToggleShaders() {
|
||||
Screen::get()->toggleShaders();
|
||||
Notifier::get()->show({Locale::get()->get(Options::video.shader.enabled ? "ui.shaders_enabled" : "ui.shaders_disabled")}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handleNextShaderPreset() {
|
||||
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
|
||||
if (!Options::crtpi_presets.empty()) {
|
||||
Options::video.shader.current_crtpi_preset = (Options::video.shader.current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
|
||||
Screen::get()->reloadCrtPi();
|
||||
Notifier::get()->show({Locale::get()->get("ui.crtpi") + " " + prettyName(Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)].name)}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
} else {
|
||||
if (!Options::postfx_presets.empty()) {
|
||||
Options::video.shader.current_postfx_preset = (Options::video.shader.current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
|
||||
Screen::get()->reloadPostFX();
|
||||
Notifier::get()->show({Locale::get()->get("ui.postfx") + " " + prettyName(Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)].name)}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleNextShader() {
|
||||
Screen::get()->nextShader();
|
||||
Notifier::get()->show({Locale::get()->get("ui.shader") + " " + // NOLINT(readability-static-accessed-through-instance)
|
||||
(Options::video.shader.current_shader == Rendering::ShaderType::CRTPI ? "CRTPI" : "POSTFX")});
|
||||
}
|
||||
|
||||
void handleNextPalette() {
|
||||
Screen::get()->nextPalette();
|
||||
Notifier::get()->show({Locale::get()->get("ui.palette") + " " + toUpper(Screen::get()->getPalettePrettyName())}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handlePreviousPalette() {
|
||||
Screen::get()->previousPalette();
|
||||
Notifier::get()->show({Locale::get()->get("ui.palette") + " " + toUpper(Screen::get()->getPalettePrettyName())}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handleNextPaletteSortMode() {
|
||||
Screen::get()->nextPaletteSortMode();
|
||||
Notifier::get()->show({Locale::get()->get("ui.palette_sort") + " " + toUpper(Screen::get()->getPaletteSortModeName())}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handleToggleIntegerScale() {
|
||||
Screen::get()->toggleIntegerScale();
|
||||
Screen::get()->setVideoMode(Options::video.fullscreen);
|
||||
Notifier::get()->show({Locale::get()->get(Options::video.integer_scale ? "ui.integer_scale_enabled" : "ui.integer_scale_disabled")}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
void handleToggleVSync() {
|
||||
Screen::get()->toggleVSync();
|
||||
Notifier::get()->show({Locale::get()->get(Options::video.vertical_sync ? "ui.vsync_enabled" : "ui.vsync_disabled")}); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
// Detecta qué acción global ha sido presionada (si alguna)
|
||||
auto getPressedAction() -> InputAction { // NOLINT(readability-function-cognitive-complexity)
|
||||
if (Input::get()->checkAction(InputAction::EXIT, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::EXIT;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::ACCEPT, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::ACCEPT;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_BORDER, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::TOGGLE_BORDER;
|
||||
}
|
||||
if (!Options::kiosk.enabled) {
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_FULLSCREEN, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::TOGGLE_FULLSCREEN;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::WINDOW_DEC_ZOOM, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::WINDOW_DEC_ZOOM;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::WINDOW_INC_ZOOM, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::WINDOW_INC_ZOOM;
|
||||
}
|
||||
}
|
||||
if (Screen::get()->isHardwareAccelerated()) {
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_SHADER, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) {
|
||||
return InputAction::TOGGLE_SUPERSAMPLING; // Ctrl+F4
|
||||
}
|
||||
if (Options::video.shader.enabled && ((SDL_GetModState() & SDL_KMOD_SHIFT) != 0U)) {
|
||||
return InputAction::NEXT_SHADER_PRESET; // Shift+F4
|
||||
}
|
||||
return InputAction::TOGGLE_SHADER; // F4
|
||||
}
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::NEXT_PALETTE, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) {
|
||||
return InputAction::PREVIOUS_PALETTE; // Ctrl+F5
|
||||
}
|
||||
return InputAction::NEXT_PALETTE; // F5
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::NEXT_PALETTE_SORT, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::NEXT_PALETTE_SORT; // F6
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_INTEGER_SCALE, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::TOGGLE_INTEGER_SCALE;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_VSYNC, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::TOGGLE_VSYNC;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_INFO, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::TOGGLE_INFO;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_CONSOLE, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
return InputAction::TOGGLE_CONSOLE;
|
||||
}
|
||||
return InputAction::NONE;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Funciones públicas
|
||||
|
||||
// Comprueba los inputs que se pueden introducir en cualquier sección del juego
|
||||
void handle() {
|
||||
const bool CONSOLE_ACTIVE = Console::get() != nullptr && Console::get()->isActive();
|
||||
|
||||
if (CONSOLE_ACTIVE) {
|
||||
// TAB/ESC cierran la consola en lugar de ejecutar sus acciones normales
|
||||
if (Input::get()->checkAction(InputAction::TOGGLE_CONSOLE, Input::DO_NOT_ALLOW_REPEAT) ||
|
||||
Input::get()->checkAction(InputAction::EXIT, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
Console::get()->toggle();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Salida de administrador en modo kiosko (Ctrl+Shift+Alt+Q)
|
||||
if (Options::kiosk.enabled) {
|
||||
SDL_Keymod mod = SDL_GetModState();
|
||||
const bool* ks = SDL_GetKeyboardState(nullptr);
|
||||
if (((mod & SDL_KMOD_CTRL) != 0U) && ((mod & SDL_KMOD_SHIFT) != 0U) && ((mod & SDL_KMOD_ALT) != 0U) && ks[SDL_SCANCODE_Q]) {
|
||||
SceneManager::current = SceneManager::Scene::QUIT;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detectar qué acción global está siendo presionada
|
||||
InputAction action = getPressedAction();
|
||||
|
||||
// Con consola activa, ACCEPT (saltar sección) y EXIT están bloqueados
|
||||
if (CONSOLE_ACTIVE && (action == InputAction::ACCEPT || action == InputAction::EXIT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ejecutar el handler correspondiente usando switch statement
|
||||
switch (action) {
|
||||
case InputAction::EXIT:
|
||||
handleQuit();
|
||||
break;
|
||||
|
||||
case InputAction::ACCEPT:
|
||||
handleSkipSection();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_BORDER:
|
||||
handleToggleBorder();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_FULLSCREEN:
|
||||
handleToggleVideoMode();
|
||||
break;
|
||||
|
||||
case InputAction::WINDOW_DEC_ZOOM:
|
||||
handleDecWindowZoom();
|
||||
break;
|
||||
|
||||
case InputAction::WINDOW_INC_ZOOM:
|
||||
handleIncWindowZoom();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_SHADER:
|
||||
handleToggleShaders();
|
||||
break;
|
||||
|
||||
case InputAction::NEXT_SHADER_PRESET:
|
||||
handleNextShaderPreset();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_SUPERSAMPLING:
|
||||
handleNextShader();
|
||||
break;
|
||||
|
||||
case InputAction::NEXT_PALETTE:
|
||||
handleNextPalette();
|
||||
break;
|
||||
|
||||
case InputAction::PREVIOUS_PALETTE:
|
||||
handlePreviousPalette();
|
||||
break;
|
||||
|
||||
case InputAction::NEXT_PALETTE_SORT:
|
||||
handleNextPaletteSortMode();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_INTEGER_SCALE:
|
||||
handleToggleIntegerScale();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_VSYNC:
|
||||
handleToggleVSync();
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_CONSOLE:
|
||||
if (Console::get() != nullptr) { Console::get()->toggle(); }
|
||||
break;
|
||||
|
||||
case InputAction::TOGGLE_INFO:
|
||||
if (RenderInfo::get() != nullptr) { RenderInfo::get()->toggle(); }
|
||||
break;
|
||||
|
||||
case InputAction::NONE:
|
||||
default:
|
||||
// No se presionó ninguna acción global
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // namespace GlobalInputs
|
||||
6
source/core/input/global_inputs.hpp
Normal file
6
source/core/input/global_inputs.hpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
namespace GlobalInputs {
|
||||
// Comprueba los inputs que se pueden introducir en cualquier sección del juego
|
||||
void handle();
|
||||
} // namespace GlobalInputs
|
||||
478
source/core/input/input.cpp
Normal file
478
source/core/input/input.cpp
Normal file
@@ -0,0 +1,478 @@
|
||||
#include "core/input/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
|
||||
|
||||
#include "game/options.hpp" // Para Options::controls
|
||||
|
||||
// Singleton
|
||||
Input* Input::instance = nullptr;
|
||||
|
||||
// Inicializa la instancia única del singleton
|
||||
void Input::init(const std::string& game_controller_db_path) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
Input::instance = new Input(game_controller_db_path);
|
||||
}
|
||||
|
||||
// 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)
|
||||
: gamepad_mappings_file_(std::move(game_controller_db_path)) {
|
||||
// Inicializar bindings del teclado
|
||||
keyboard_.bindings = {
|
||||
// Movimiento del jugador
|
||||
{Action::LEFT, KeyState{.scancode = SDL_SCANCODE_LEFT}},
|
||||
{Action::RIGHT, KeyState{.scancode = SDL_SCANCODE_RIGHT}},
|
||||
{Action::JUMP, KeyState{.scancode = SDL_SCANCODE_UP}},
|
||||
|
||||
// Inputs de control
|
||||
{Action::ACCEPT, KeyState{.scancode = SDL_SCANCODE_RETURN}},
|
||||
{Action::CANCEL, KeyState{.scancode = SDL_SCANCODE_ESCAPE}},
|
||||
{Action::EXIT, KeyState{.scancode = SDL_SCANCODE_ESCAPE}},
|
||||
|
||||
// Inputs de sistema
|
||||
{Action::WINDOW_DEC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F1}},
|
||||
{Action::WINDOW_INC_ZOOM, KeyState{.scancode = SDL_SCANCODE_F2}},
|
||||
{Action::TOGGLE_FULLSCREEN, KeyState{.scancode = SDL_SCANCODE_F3}},
|
||||
{Action::TOGGLE_SHADER, KeyState{.scancode = SDL_SCANCODE_F4}},
|
||||
{Action::NEXT_PALETTE, KeyState{.scancode = SDL_SCANCODE_F5}},
|
||||
{Action::NEXT_PALETTE_SORT, KeyState{.scancode = SDL_SCANCODE_F6}},
|
||||
{Action::TOGGLE_INTEGER_SCALE, KeyState{.scancode = SDL_SCANCODE_F7}},
|
||||
{Action::TOGGLE_IN_GAME_MUSIC, KeyState{.scancode = SDL_SCANCODE_F8}},
|
||||
{Action::TOGGLE_BORDER, KeyState{.scancode = SDL_SCANCODE_F9}},
|
||||
{Action::TOGGLE_VSYNC, KeyState{.scancode = SDL_SCANCODE_F10}},
|
||||
{Action::PAUSE, KeyState{.scancode = SDL_SCANCODE_F11}},
|
||||
{Action::TOGGLE_INFO, KeyState{.scancode = SDL_SCANCODE_F12}},
|
||||
{Action::TOGGLE_CONSOLE, KeyState{.scancode = SDL_SCANCODE_GRAVE}}};
|
||||
|
||||
initSDLGamePad(); // Inicializa el subsistema SDL_INIT_GAMEPAD
|
||||
}
|
||||
|
||||
// Asigna inputs a teclas
|
||||
void Input::bindKey(Action action, SDL_Scancode code) {
|
||||
keyboard_.bindings[action].scancode = code;
|
||||
}
|
||||
|
||||
// Aplica las teclas configuradas desde Options
|
||||
void Input::applyKeyboardBindingsFromOptions() {
|
||||
bindKey(Action::LEFT, Options::keyboard_controls.key_left);
|
||||
bindKey(Action::RIGHT, Options::keyboard_controls.key_right);
|
||||
bindKey(Action::JUMP, Options::keyboard_controls.key_jump);
|
||||
}
|
||||
|
||||
// Aplica configuración de botones del gamepad desde Options al primer gamepad conectado
|
||||
void Input::applyGamepadBindingsFromOptions() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Si no hay gamepads conectados, no hay nada que hacer
|
||||
if (gamepads_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtener el primer gamepad conectado
|
||||
const auto& gamepad = gamepads_[0];
|
||||
|
||||
// Aplicar bindings desde Options
|
||||
// Los valores pueden ser:
|
||||
// - 0-20+: Botones SDL_GamepadButton (DPAD, face buttons, shoulders)
|
||||
// - 100: L2 trigger
|
||||
// - 101: R2 trigger
|
||||
// - 200+: Ejes del stick analógico
|
||||
gamepad->bindings[Action::LEFT].button = Options::gamepad_controls.button_left;
|
||||
gamepad->bindings[Action::RIGHT].button = Options::gamepad_controls.button_right;
|
||||
gamepad->bindings[Action::JUMP].button = Options::gamepad_controls.button_jump;
|
||||
}
|
||||
|
||||
// Asigna inputs a botones del mando
|
||||
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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 { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Si gamepad es nullptr pero hay mandos conectados, usar el primero
|
||||
std::shared_ptr<Gamepad> active_gamepad = gamepad;
|
||||
if (active_gamepad == nullptr && !gamepads_.empty()) {
|
||||
active_gamepad = gamepads_[0];
|
||||
}
|
||||
|
||||
if (active_gamepad != nullptr) {
|
||||
success_controller = checkAxisInput(action, active_gamepad, repeat);
|
||||
|
||||
if (!success_controller) {
|
||||
success_controller = checkTriggerInput(action, active_gamepad, repeat);
|
||||
}
|
||||
|
||||
if (!success_controller) {
|
||||
if (repeat) { // El usuario quiere saber si está pulsada (estado mantenido)
|
||||
success_controller = active_gamepad->bindings[action].is_held;
|
||||
} else { // El usuario quiere saber si ACABA de ser pulsada (evento de un solo fotograma)
|
||||
success_controller = active_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 { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si gamepad es nullptr pero hay mandos conectados, usar el primero
|
||||
std::shared_ptr<Gamepad> active_gamepad = gamepad;
|
||||
if (active_gamepad == nullptr && !gamepads_.empty()) {
|
||||
active_gamepad = gamepads_[0];
|
||||
}
|
||||
|
||||
// --- Comprobación del Mando ---
|
||||
// Comprobamos si hay mandos y si el índice solicitado es válido.
|
||||
if (active_gamepad != nullptr) {
|
||||
// Iteramos sobre todas las acciones, no sobre el número de mandos.
|
||||
for (const auto& pair : active_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 { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// 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> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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 { // NOLINT(readability-convert-member-functions-to-static)
|
||||
return static_cast<SDL_GamepadButton>(gamepad->bindings[action].button);
|
||||
}
|
||||
|
||||
// Comprueba el eje del mando
|
||||
auto Input::checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Obtener el binding configurado para esta acción
|
||||
auto& binding = gamepad->bindings[action];
|
||||
|
||||
// Solo revisar ejes si el binding está configurado como eje (valores 200+)
|
||||
// 200 = Left stick izquierda, 201 = Left stick derecha
|
||||
if (binding.button < 200) {
|
||||
// El binding no es un eje, no revisar axis
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determinar qué eje y dirección revisar según el binding
|
||||
bool axis_active_now = false;
|
||||
|
||||
if (binding.button == 200) {
|
||||
// Left stick izquierda
|
||||
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) < -AXIS_THRESHOLD;
|
||||
} else if (binding.button == 201) {
|
||||
// Left stick derecha
|
||||
axis_active_now = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX) > AXIS_THRESHOLD;
|
||||
} else {
|
||||
// Binding de eje no soportado
|
||||
return false;
|
||||
}
|
||||
|
||||
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 { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// 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() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
handleEvent(event); // Comprueba mandos conectados
|
||||
}
|
||||
}
|
||||
|
||||
void Input::initSDLGamePad() {
|
||||
if (SDL_WasInit(SDL_INIT_GAMEPAD) != 1) {
|
||||
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_GAMEPAD could not initialize! SDL Error: %s", SDL_GetError());
|
||||
} else {
|
||||
addGamepadMappingsFromFile();
|
||||
discoverGamepads();
|
||||
std::cout << "\n** INPUT SYSTEM **\n";
|
||||
std::cout << "Input System initialized successfully\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 (const 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() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// --- 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 { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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 { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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';
|
||||
gamepads_.push_back(std::move(gamepad));
|
||||
return name + " CONNECTED";
|
||||
}
|
||||
|
||||
auto Input::removeGamepad(SDL_JoystickID id) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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 { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Input::Gamepad> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// 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;
|
||||
}
|
||||
140
source/core/input/input.hpp
Normal file
140
source/core/input/input.hpp
Normal file
@@ -0,0 +1,140 @@
|
||||
#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 "core/input/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{0}; // Scancode asociado
|
||||
bool is_held{false}; // Está pulsada ahora mismo
|
||||
bool just_pressed{false}; // Se acaba de pulsar en este fotograma
|
||||
};
|
||||
|
||||
struct ButtonState {
|
||||
int button{static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)}; // GameControllerButton asociado
|
||||
bool is_held{false}; // Está pulsada ahora mismo
|
||||
bool just_pressed{false}; // Se acaba de pulsar en este fotograma
|
||||
bool axis_active{false}; // Estado del eje
|
||||
bool trigger_active{false}; // Estado del trigger como botón digital
|
||||
};
|
||||
|
||||
struct Keyboard {
|
||||
std::unordered_map<Action, KeyState> bindings; // Mapa de acciones a estados de tecla
|
||||
};
|
||||
|
||||
struct Gamepad {
|
||||
SDL_Gamepad* pad{nullptr}; // Puntero al gamepad SDL
|
||||
SDL_JoystickID instance_id{0}; // ID de instancia del joystick
|
||||
std::string name; // Nombre del gamepad
|
||||
std::string path; // Ruta del dispositivo
|
||||
std::unordered_map<Action, ButtonState> bindings; // Mapa de acciones a estados de botón
|
||||
|
||||
explicit 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::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
|
||||
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
|
||||
{Action::JUMP, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}}} {}
|
||||
|
||||
~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].button = static_cast<int>(new_button);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Tipos ---
|
||||
using Gamepads = std::vector<std::shared_ptr<Gamepad>>; // Vector de gamepads
|
||||
|
||||
// --- Singleton ---
|
||||
static void init(const std::string& game_controller_db_path);
|
||||
static void destroy();
|
||||
static auto get() -> Input*;
|
||||
|
||||
// --- Actualización del sistema ---
|
||||
void update(); // Actualiza estados de entrada
|
||||
|
||||
// --- Configuración de controles ---
|
||||
void bindKey(Action action, SDL_Scancode code);
|
||||
void applyKeyboardBindingsFromOptions();
|
||||
void applyGamepadBindingsFromOptions();
|
||||
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);
|
||||
|
||||
// --- Consulta de entrada ---
|
||||
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;
|
||||
void resetInputStates();
|
||||
|
||||
// --- Gestión de gamepads ---
|
||||
[[nodiscard]] auto gameControllerFound() const -> bool;
|
||||
[[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_; }
|
||||
auto findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Gamepad>;
|
||||
static auto getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::string;
|
||||
[[nodiscard]] auto getControllerNames() const -> std::vector<std::string>;
|
||||
[[nodiscard]] static auto getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton;
|
||||
void printConnectedGamepads() const;
|
||||
|
||||
// --- Eventos ---
|
||||
auto handleEvent(const SDL_Event& event) -> std::string;
|
||||
|
||||
private:
|
||||
// --- Constantes ---
|
||||
static constexpr Sint16 AXIS_THRESHOLD = 30000; // Umbral para ejes analógicos
|
||||
static constexpr Sint16 TRIGGER_THRESHOLD = 16384; // Umbral para triggers (50% del rango)
|
||||
static constexpr std::array<Action, 1> BUTTON_INPUTS = {Action::JUMP}; // Inputs que usan botones
|
||||
|
||||
// --- Métodos ---
|
||||
explicit Input(std::string game_controller_db_path);
|
||||
~Input() = default;
|
||||
|
||||
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();
|
||||
|
||||
// --- Variables miembro ---
|
||||
static Input* instance; // Instancia única del singleton
|
||||
|
||||
Gamepads gamepads_; // Lista de gamepads conectados
|
||||
Keyboard keyboard_{}; // Estado del teclado
|
||||
std::string gamepad_mappings_file_; // Ruta al archivo de mappings
|
||||
};
|
||||
82
source/core/input/input_types.cpp
Normal file
82
source/core/input/input_types.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include "input_types.hpp"
|
||||
|
||||
#include <utility> // Para pair
|
||||
|
||||
// Definición de los mapas
|
||||
const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
|
||||
{InputAction::LEFT, "LEFT"},
|
||||
{InputAction::RIGHT, "RIGHT"},
|
||||
{InputAction::JUMP, "JUMP"},
|
||||
{InputAction::PAUSE, "PAUSE"},
|
||||
{InputAction::EXIT, "EXIT"},
|
||||
{InputAction::ACCEPT, "ACCEPT"},
|
||||
{InputAction::CANCEL, "CANCEL"},
|
||||
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
|
||||
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
|
||||
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
|
||||
{InputAction::TOGGLE_VSYNC, "TOGGLE_VSYNC"},
|
||||
{InputAction::TOGGLE_INTEGER_SCALE, "TOGGLE_INTEGER_SCALE"},
|
||||
{InputAction::TOGGLE_BORDER, "TOGGLE_BORDER"},
|
||||
{InputAction::TOGGLE_IN_GAME_MUSIC, "TOGGLE_MUSIC"},
|
||||
{InputAction::NEXT_PALETTE, "NEXT_PALETTE"},
|
||||
{InputAction::PREVIOUS_PALETTE, "PREVIOUS_PALETTE"},
|
||||
{InputAction::NEXT_PALETTE_SORT, "NEXT_PALETTE_SORT"},
|
||||
{InputAction::TOGGLE_SHADER, "TOGGLE_POSTFX"},
|
||||
{InputAction::NEXT_SHADER_PRESET, "NEXT_POSTFX_PRESET"},
|
||||
{InputAction::TOGGLE_INFO, "TOGGLE_DEBUG"},
|
||||
{InputAction::NONE, "NONE"}};
|
||||
|
||||
const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
|
||||
{"LEFT", InputAction::LEFT},
|
||||
{"RIGHT", InputAction::RIGHT},
|
||||
{"JUMP", InputAction::JUMP},
|
||||
{"PAUSE", InputAction::PAUSE},
|
||||
{"EXIT", InputAction::EXIT},
|
||||
{"ACCEPT", InputAction::ACCEPT},
|
||||
{"CANCEL", InputAction::CANCEL},
|
||||
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
|
||||
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
|
||||
{"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN},
|
||||
{"TOGGLE_VSYNC", InputAction::TOGGLE_VSYNC},
|
||||
{"TOGGLE_INTEGER_SCALE", InputAction::TOGGLE_INTEGER_SCALE},
|
||||
{"TOGGLE_BORDER", InputAction::TOGGLE_BORDER},
|
||||
{"TOGGLE_MUSIC", InputAction::TOGGLE_IN_GAME_MUSIC},
|
||||
{"NEXT_PALETTE", InputAction::NEXT_PALETTE},
|
||||
{"PREVIOUS_PALETTE", InputAction::PREVIOUS_PALETTE},
|
||||
{"NEXT_PALETTE_SORT", InputAction::NEXT_PALETTE_SORT},
|
||||
{"TOGGLE_POSTFX", InputAction::TOGGLE_SHADER},
|
||||
{"NEXT_POSTFX_PRESET", InputAction::NEXT_SHADER_PRESET},
|
||||
{"TOGGLE_DEBUG", InputAction::TOGGLE_INFO},
|
||||
{"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)}};
|
||||
47
source/core/input/input_types.hpp
Normal file
47
source/core/input/input_types.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#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
|
||||
LEFT,
|
||||
RIGHT,
|
||||
JUMP,
|
||||
|
||||
// Inputs de control
|
||||
PAUSE,
|
||||
EXIT,
|
||||
ACCEPT,
|
||||
CANCEL,
|
||||
|
||||
// Inputs de sistema
|
||||
WINDOW_INC_ZOOM,
|
||||
WINDOW_DEC_ZOOM,
|
||||
TOGGLE_FULLSCREEN,
|
||||
TOGGLE_VSYNC,
|
||||
TOGGLE_INTEGER_SCALE,
|
||||
TOGGLE_SHADER,
|
||||
NEXT_SHADER_PRESET,
|
||||
TOGGLE_SUPERSAMPLING,
|
||||
TOGGLE_BORDER,
|
||||
TOGGLE_IN_GAME_MUSIC,
|
||||
NEXT_PALETTE,
|
||||
PREVIOUS_PALETTE,
|
||||
NEXT_PALETTE_SORT,
|
||||
TOGGLE_INFO,
|
||||
TOGGLE_CONSOLE,
|
||||
|
||||
// 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
|
||||
25
source/core/input/mouse.cpp
Normal file
25
source/core/input/mouse.cpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#include "core/input/mouse.hpp"
|
||||
|
||||
namespace Mouse {
|
||||
Uint32 cursor_hide_time = 3000; // Tiempo en milisegundos para ocultar el cursor
|
||||
Uint32 last_mouse_move_time = 0; // Última vez que el ratón se movió
|
||||
bool cursor_visible = true; // Estado del cursor
|
||||
|
||||
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
|
||||
12
source/core/input/mouse.hpp
Normal file
12
source/core/input/mouse.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace Mouse {
|
||||
extern Uint32 cursor_hide_time; // Tiempo en milisegundos para ocultar el cursor
|
||||
extern Uint32 last_mouse_move_time; // Última vez que el ratón se movió
|
||||
extern bool cursor_visible; // Estado del cursor
|
||||
|
||||
void handleEvent(const SDL_Event& event);
|
||||
void updateCursorVisibility();
|
||||
} // namespace Mouse
|
||||
104
source/core/locale/locale.cpp
Normal file
104
source/core/locale/locale.cpp
Normal file
@@ -0,0 +1,104 @@
|
||||
#include "core/locale/locale.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||
|
||||
// [SINGLETON]
|
||||
Locale* Locale::instance = nullptr;
|
||||
|
||||
// [SINGLETON] Crea el objeto con esta función estática
|
||||
void Locale::init(const std::string& file_path) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
Locale::instance = new Locale();
|
||||
Locale::instance->loadFromFile(file_path);
|
||||
}
|
||||
|
||||
// [SINGLETON] Crea el objeto desde contenido en memoria (para release con pack)
|
||||
void Locale::initFromContent(const std::string& content) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
Locale::instance = new Locale();
|
||||
Locale::instance->loadFromContent(content);
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruye el objeto con esta función estática
|
||||
void Locale::destroy() {
|
||||
delete Locale::instance;
|
||||
Locale::instance = nullptr;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto Locale::get() -> Locale* {
|
||||
return Locale::instance;
|
||||
}
|
||||
|
||||
// Devuelve la traducción de la clave o la clave como fallback
|
||||
auto Locale::get(const std::string& key) const -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = strings_.find(key);
|
||||
if (it != strings_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::cerr << "Locale: clave no encontrada: " << key << '\n';
|
||||
return key;
|
||||
}
|
||||
|
||||
// Aplana un nodo YAML de forma recursiva: {a: {b: "val"}} -> {"a.b" -> "val"}
|
||||
void Locale::flatten(const void* node_ptr, const std::string& prefix) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const auto& node = *static_cast<const fkyaml::node*>(node_ptr);
|
||||
|
||||
for (auto itr = node.begin(); itr != node.end(); ++itr) {
|
||||
const std::string KEY = prefix.empty()
|
||||
? itr.key().get_value<std::string>()
|
||||
: prefix + "." + itr.key().get_value<std::string>();
|
||||
|
||||
const auto& value = itr.value();
|
||||
if (value.is_mapping()) {
|
||||
flatten(&value, KEY);
|
||||
} else if (value.is_string()) {
|
||||
strings_[KEY] = value.get_value<std::string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga las traducciones desde contenido YAML en memoria
|
||||
void Locale::loadFromContent(const std::string& content) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (content.empty()) {
|
||||
std::cerr << "Locale: contenido vacío, sin traducciones cargadas\n";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
std::istringstream stream(content);
|
||||
auto yaml = fkyaml::node::deserialize(stream);
|
||||
flatten(&yaml, "");
|
||||
|
||||
std::cout << "Locale: " << strings_.size() << " traducciones cargadas desde pack\n";
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "Locale: error al parsear YAML: " << e.what() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Carga las traducciones desde el fichero YAML indicado
|
||||
void Locale::loadFromFile(const std::string& file_path) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (file_path.empty()) {
|
||||
std::cerr << "Locale: ruta de fichero vacía, sin traducciones cargadas\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::ifstream file(file_path);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "Locale: no se puede abrir " << file_path << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(file);
|
||||
flatten(&yaml, "");
|
||||
|
||||
std::cout << "Locale: " << strings_.size() << " traducciones cargadas desde " << file_path << '\n';
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "Locale: error al parsear YAML: " << e.what() << '\n';
|
||||
}
|
||||
}
|
||||
28
source/core/locale/locale.hpp
Normal file
28
source/core/locale/locale.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
// Clase Locale: gestiona las traducciones del juego (singleton)
|
||||
// Las traducciones se cargan desde un fichero YAML en el inicio.
|
||||
// No se permite cambio de idioma en caliente.
|
||||
class Locale {
|
||||
public:
|
||||
static void init(const std::string& file_path); // Crea e inicializa el singleton
|
||||
static void initFromContent(const std::string& content); // Crea e inicializa desde contenido en memoria (pack)
|
||||
static void destroy(); // Destruye el singleton
|
||||
static auto get() -> Locale*; // Devuelve el singleton
|
||||
|
||||
// Devuelve la traducción de la clave dada.
|
||||
// Si la clave no existe, devuelve la propia clave como fallback.
|
||||
[[nodiscard]] auto get(const std::string& key) const -> std::string;
|
||||
|
||||
private:
|
||||
Locale() = default;
|
||||
void loadFromFile(const std::string& file_path);
|
||||
void loadFromContent(const std::string& content);
|
||||
void flatten(const void* node_ptr, const std::string& prefix); // Aplana nodos YAML anidados
|
||||
|
||||
static Locale* instance;
|
||||
std::unordered_map<std::string, std::string> strings_;
|
||||
};
|
||||
295
source/core/rendering/gif.cpp
Normal file
295
source/core/rendering/gif.cpp
Normal file
@@ -0,0 +1,295 @@
|
||||
#include "core/rendering/gif.hpp"
|
||||
|
||||
#include <cstring> // Para memcpy, size_t
|
||||
#include <iostream> // Para std::cout
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <string> // Para allocator, char_traits, operator==, basic_string
|
||||
|
||||
namespace GIF {
|
||||
|
||||
// Función inline para reemplazar el macro READ.
|
||||
// Actualiza el puntero 'buffer' tras copiar 'size' bytes a 'dst'.
|
||||
inline void readBytes(const uint8_t*& buffer, void* dst, size_t size) {
|
||||
std::memcpy(dst, buffer, size);
|
||||
buffer += size;
|
||||
}
|
||||
|
||||
// Inicializa el diccionario LZW con los valores iniciales
|
||||
inline void initializeDictionary(std::vector<DictionaryEntry>& dictionary, int code_length, int& dictionary_ind) { // NOLINT(readability-identifier-naming)
|
||||
int size = 1 << code_length;
|
||||
dictionary.resize(1 << (code_length + 1));
|
||||
for (dictionary_ind = 0; dictionary_ind < size; 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; // Reservamos espacio para clear y stop codes
|
||||
}
|
||||
|
||||
// Lee los próximos bits del stream de entrada para formar un código
|
||||
inline auto readNextCode(const uint8_t*& input, int& input_length, unsigned int& mask, int code_length) -> int {
|
||||
int code = 0;
|
||||
for (int i = 0; i < (code_length + 1); i++) {
|
||||
if (input_length <= 0) {
|
||||
throw std::runtime_error("Unexpected end of input in decompress");
|
||||
}
|
||||
int bit = ((*input & mask) != 0) ? 1 : 0;
|
||||
mask <<= 1;
|
||||
if (mask == 0x100) {
|
||||
mask = 0x01;
|
||||
input++;
|
||||
input_length--;
|
||||
}
|
||||
code |= (bit << i);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
// Encuentra el primer byte de una cadena del diccionario
|
||||
inline auto findFirstByte(const std::vector<DictionaryEntry>& dictionary, int code) -> uint8_t {
|
||||
int ptr = code;
|
||||
while (dictionary[ptr].prev != -1) {
|
||||
ptr = dictionary[ptr].prev;
|
||||
}
|
||||
return dictionary[ptr].byte;
|
||||
}
|
||||
|
||||
// Agrega una nueva entrada al diccionario
|
||||
inline void addDictionaryEntry(std::vector<DictionaryEntry>& dictionary, int& dictionary_ind, int& code_length, int prev, int code) { // NOLINT(readability-identifier-naming)
|
||||
uint8_t first_byte;
|
||||
if (code == dictionary_ind) {
|
||||
first_byte = findFirstByte(dictionary, prev);
|
||||
} else {
|
||||
first_byte = findFirstByte(dictionary, code);
|
||||
}
|
||||
|
||||
dictionary[dictionary_ind].byte = first_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));
|
||||
}
|
||||
}
|
||||
|
||||
// Escribe la cadena decodificada al buffer de salida
|
||||
inline auto writeDecodedString(const std::vector<DictionaryEntry>& dictionary, int code, uint8_t*& out) -> int {
|
||||
int cur_code = code;
|
||||
int match_len = dictionary[cur_code].len;
|
||||
while (cur_code != -1) {
|
||||
out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte;
|
||||
if (dictionary[cur_code].prev == cur_code) {
|
||||
std::cerr << "Internal error; self-reference detected." << '\n';
|
||||
throw std::runtime_error("Internal error in decompress: self-reference");
|
||||
}
|
||||
cur_code = dictionary[cur_code].prev;
|
||||
}
|
||||
out += match_len;
|
||||
return match_len;
|
||||
}
|
||||
|
||||
void Gif::decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Verifica que el code_length tenga un rango razonable.
|
||||
if (code_length < 2 || code_length > 12) {
|
||||
throw std::runtime_error("Invalid LZW code length");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Inicializamos el diccionario con el tamaño correspondiente.
|
||||
initializeDictionary(dictionary, code_length, dictionary_ind);
|
||||
|
||||
// Bucle principal: procesar el stream comprimido.
|
||||
while (input_length > 0) {
|
||||
int code = readNextCode(input, input_length, mask, code_length);
|
||||
|
||||
if (code == clear_code) {
|
||||
// Reinicia el diccionario.
|
||||
code_length = reset_code_length;
|
||||
initializeDictionary(dictionary, code_length, dictionary_ind);
|
||||
prev = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code == stop_code) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (prev > -1 && code_length < 12) {
|
||||
if (code > dictionary_ind) {
|
||||
std::cerr << "code = " << std::hex << code
|
||||
<< ", but dictionary_ind = " << dictionary_ind << '\n';
|
||||
throw std::runtime_error("LZW error: code exceeds dictionary_ind.");
|
||||
}
|
||||
|
||||
addDictionaryEntry(dictionary, dictionary_ind, code_length, prev, code);
|
||||
}
|
||||
|
||||
prev = code;
|
||||
|
||||
// Verifica que 'code' sea un índice válido antes de usarlo.
|
||||
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
|
||||
std::cerr << "Invalid LZW code " << code
|
||||
<< ", dictionary size " << dictionary.size() << '\n';
|
||||
throw std::runtime_error("LZW error: invalid code encountered");
|
||||
}
|
||||
|
||||
writeDecodedString(dictionary, code, out);
|
||||
}
|
||||
}
|
||||
|
||||
auto Gif::readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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;
|
||||
}
|
||||
|
||||
auto Gif::processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& gct, int resolution_bits) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
ImageDescriptor image_descriptor;
|
||||
// Lee 9 bytes para el 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;
|
||||
}
|
||||
|
||||
auto Gif::loadPalette(const uint8_t* buffer) -> std::vector<uint32_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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) != 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
auto Gif::processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Leer la cabecera de 6 bytes ("GIF87a" o "GIF89a")
|
||||
uint8_t header[6];
|
||||
std::memcpy(header, buffer, 6);
|
||||
buffer += 6;
|
||||
|
||||
// Opcional: Validar header
|
||||
std::string header_str(reinterpret_cast<char*>(header), 6);
|
||||
if (header_str != "GIF87a" && header_str != "GIF89a") {
|
||||
throw std::runtime_error("Formato de archivo GIF inválido.");
|
||||
}
|
||||
|
||||
// Leer el Screen Descriptor (7 bytes, empaquetado sin padding)
|
||||
ScreenDescriptor screen_descriptor;
|
||||
readBytes(buffer, &screen_descriptor, sizeof(ScreenDescriptor));
|
||||
|
||||
// Asigna ancho y alto
|
||||
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) != 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Supongamos que 'buffer' es el puntero actual y TRAILER es 0x3B
|
||||
uint8_t block_type = *buffer++;
|
||||
while (block_type != TRAILER) {
|
||||
if (block_type == EXTENSION_INTRODUCER) // 0x21
|
||||
{
|
||||
// Se lee la etiqueta de extensión, la cual indica el tipo de extensión.
|
||||
uint8_t extension_label = *buffer++;
|
||||
switch (extension_label) {
|
||||
case GRAPHIC_CONTROL: // 0xF9
|
||||
{
|
||||
// Procesar Graphic Control Extension:
|
||||
uint8_t block_size = *buffer++; // Normalmente, blockSize == 4
|
||||
buffer += block_size; // Saltamos los 4 bytes del bloque fijo
|
||||
// Saltar los sub-bloques
|
||||
uint8_t sub_block_size = *buffer++;
|
||||
while (sub_block_size != 0) {
|
||||
buffer += sub_block_size;
|
||||
sub_block_size = *buffer++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case APPLICATION_EXTENSION: // 0xFF
|
||||
case COMMENT_EXTENSION: // 0xFE
|
||||
case PLAINTEXT_EXTENSION: // 0x01
|
||||
{
|
||||
// Para estas extensiones, saltamos el bloque fijo y los sub-bloques.
|
||||
uint8_t block_size = *buffer++;
|
||||
buffer += block_size;
|
||||
uint8_t sub_block_size = *buffer++;
|
||||
while (sub_block_size != 0) {
|
||||
buffer += sub_block_size;
|
||||
sub_block_size = *buffer++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Si la etiqueta de extensión es desconocida, saltarla también:
|
||||
uint8_t block_size = *buffer++;
|
||||
buffer += block_size;
|
||||
uint8_t sub_block_size = *buffer++;
|
||||
while (sub_block_size != 0) {
|
||||
buffer += sub_block_size;
|
||||
sub_block_size = *buffer++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (block_type == IMAGE_DESCRIPTOR) {
|
||||
// Procesar el Image Descriptor y retornar los datos de imagen
|
||||
return processImageDescriptor(buffer, global_color_table, color_resolution_bits);
|
||||
} else {
|
||||
std::cerr << "Unrecognized block type " << std::hex << static_cast<int>(block_type) << '\n';
|
||||
return std::vector<uint8_t>{};
|
||||
}
|
||||
block_type = *buffer++;
|
||||
}
|
||||
|
||||
return std::vector<uint8_t>{};
|
||||
}
|
||||
|
||||
auto Gif::loadGif(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> {
|
||||
return processGifStream(buffer, w, h);
|
||||
}
|
||||
|
||||
} // namespace GIF
|
||||
92
source/core/rendering/gif.hpp
Normal file
92
source/core/rendering/gif.hpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint> // Para uint8_t, uint16_t, uint32_t
|
||||
#include <vector> // Para vector
|
||||
|
||||
namespace GIF {
|
||||
|
||||
// Constantes definidas con constexpr, en lugar de macros
|
||||
constexpr uint8_t EXTENSION_INTRODUCER = 0x21;
|
||||
constexpr uint8_t IMAGE_DESCRIPTOR = 0x2C;
|
||||
constexpr uint8_t TRAILER = 0x3B;
|
||||
constexpr uint8_t GRAPHIC_CONTROL = 0xF9;
|
||||
constexpr uint8_t APPLICATION_EXTENSION = 0xFF;
|
||||
constexpr uint8_t COMMENT_EXTENSION = 0xFE;
|
||||
constexpr uint8_t PLAINTEXT_EXTENSION = 0x01;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct ScreenDescriptor {
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
uint8_t fields;
|
||||
uint8_t background_color_index;
|
||||
uint8_t pixel_aspect_ratio;
|
||||
};
|
||||
|
||||
struct RGB {
|
||||
uint8_t r, g, b;
|
||||
};
|
||||
|
||||
struct ImageDescriptor {
|
||||
uint16_t image_left_position;
|
||||
uint16_t image_top_position;
|
||||
uint16_t image_width;
|
||||
uint16_t image_height;
|
||||
uint8_t fields;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct DictionaryEntry {
|
||||
uint8_t byte;
|
||||
int prev;
|
||||
int len;
|
||||
};
|
||||
|
||||
struct Extension {
|
||||
uint8_t extension_code;
|
||||
uint8_t block_size;
|
||||
};
|
||||
|
||||
struct GraphicControlExtension {
|
||||
uint8_t fields;
|
||||
uint16_t delay_time;
|
||||
uint8_t transparent_color_index;
|
||||
};
|
||||
|
||||
struct ApplicationExtension {
|
||||
uint8_t application_id[8];
|
||||
uint8_t version[3];
|
||||
};
|
||||
|
||||
struct PlaintextExtension {
|
||||
uint16_t left, top, width, height;
|
||||
uint8_t cell_width, cell_height;
|
||||
uint8_t foreground_color, background_color;
|
||||
};
|
||||
|
||||
class Gif {
|
||||
public:
|
||||
// Descompone (uncompress) el bloque comprimido usando LZW.
|
||||
// Este método puede lanzar std::runtime_error en caso de error.
|
||||
static 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).
|
||||
static auto loadPalette(const uint8_t* buffer) -> std::vector<uint32_t>;
|
||||
|
||||
// Carga el stream GIF; devuelve un vector con los datos de imagen sin comprimir y
|
||||
// asigna el ancho y alto mediante referencias.
|
||||
static auto loadGif(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t>;
|
||||
|
||||
private:
|
||||
// Lee los sub-bloques de datos y los acumula en un std::vector<uint8_t>.
|
||||
static auto readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t>;
|
||||
|
||||
// Procesa el Image Descriptor y retorna el vector de datos sin comprimir.
|
||||
static auto processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& gct, int resolution_bits) -> std::vector<uint8_t>;
|
||||
|
||||
// Procesa el stream completo del GIF y devuelve los datos sin comprimir.
|
||||
static auto processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t>;
|
||||
};
|
||||
|
||||
} // namespace GIF
|
||||
276
source/core/rendering/palette_manager.cpp
Normal file
276
source/core/rendering/palette_manager.cpp
Normal file
@@ -0,0 +1,276 @@
|
||||
#include "core/rendering/palette_manager.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "game/defaults.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "utils/utils.hpp"
|
||||
|
||||
// ── Conversión string ↔ PaletteSortMode ──────────────────────────────────────
|
||||
|
||||
auto sortModeFromString(const std::string& str) -> PaletteSortMode {
|
||||
const std::string LOWER = toLower(str);
|
||||
if (LOWER == "luminance") { return PaletteSortMode::LUMINANCE; }
|
||||
if (LOWER == "spectrum") { return PaletteSortMode::SPECTRUM; }
|
||||
return PaletteSortMode::ORIGINAL;
|
||||
}
|
||||
|
||||
auto sortModeToString(PaletteSortMode mode) -> std::string {
|
||||
switch (mode) {
|
||||
case PaletteSortMode::LUMINANCE:
|
||||
return "luminance";
|
||||
case PaletteSortMode::SPECTRUM:
|
||||
return "spectrum";
|
||||
default:
|
||||
return "original";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Paleta de referencia ZX Spectrum (16 colores ARGB) ───────────────────────
|
||||
|
||||
namespace {
|
||||
// Helpers para extraer componentes RGB de un color ARGB (0xAARRGGBB)
|
||||
constexpr auto redOf(Uint32 c) -> int { return static_cast<int>((c >> 16) & 0xFF); }
|
||||
constexpr auto greenOf(Uint32 c) -> int { return static_cast<int>((c >> 8) & 0xFF); }
|
||||
constexpr auto blueOf(Uint32 c) -> int { return static_cast<int>(c & 0xFF); }
|
||||
|
||||
constexpr auto makeARGB(int r, int g, int b) -> Uint32 {
|
||||
return (0xFFU << 24) | (static_cast<Uint32>(r) << 16) | (static_cast<Uint32>(g) << 8) | static_cast<Uint32>(b);
|
||||
}
|
||||
|
||||
// Paleta ZX Spectrum de referencia (misma que en tools/sort_palette/sort_palette.py)
|
||||
constexpr std::array<Uint32, 16> SPECTRUM_REFERENCE = {
|
||||
makeARGB(0, 0, 0),
|
||||
makeARGB(0, 0, 0),
|
||||
makeARGB(0, 0, 216),
|
||||
makeARGB(0, 0, 255),
|
||||
makeARGB(216, 0, 0),
|
||||
makeARGB(255, 0, 0),
|
||||
makeARGB(216, 0, 216),
|
||||
makeARGB(255, 0, 255),
|
||||
makeARGB(0, 216, 0),
|
||||
makeARGB(0, 255, 0),
|
||||
makeARGB(0, 216, 216),
|
||||
makeARGB(0, 255, 255),
|
||||
makeARGB(216, 216, 0),
|
||||
makeARGB(255, 255, 0),
|
||||
makeARGB(216, 216, 216),
|
||||
makeARGB(255, 255, 255),
|
||||
};
|
||||
|
||||
// Luminancia percibida (ITU-R BT.709)
|
||||
auto luminance(Uint32 color) -> double {
|
||||
return (0.2126 * redOf(color)) + (0.7152 * greenOf(color)) + (0.0722 * blueOf(color));
|
||||
}
|
||||
|
||||
// Distancia euclídea al cuadrado en espacio RGB (no necesita sqrt para comparar)
|
||||
auto rgbDistanceSq(Uint32 a, Uint32 b) -> int {
|
||||
const int DR = redOf(a) - redOf(b);
|
||||
const int DG = greenOf(a) - greenOf(b);
|
||||
const int DB = blueOf(a) - blueOf(b);
|
||||
return (DR * DR) + (DG * DG) + (DB * DB);
|
||||
}
|
||||
|
||||
// Cuenta los colores activos en la paleta (los que tienen alpha != 0)
|
||||
auto countActiveColors(const Palette& palette) -> size_t {
|
||||
size_t count = 0;
|
||||
for (const auto& c : palette) {
|
||||
if (c == 0) { break; }
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Ordenar por luminancia
|
||||
auto sortByLuminance(const Palette& palette) -> Palette {
|
||||
const size_t N = countActiveColors(palette);
|
||||
std::vector<Uint32> colors(palette.begin(), palette.begin() + static_cast<ptrdiff_t>(N));
|
||||
std::ranges::sort(colors, [](Uint32 a, Uint32 b) {
|
||||
return luminance(a) < luminance(b);
|
||||
});
|
||||
|
||||
Palette result{};
|
||||
result.fill(0);
|
||||
std::ranges::copy(colors, result.begin());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Ordenar por similitud con la paleta ZX Spectrum (greedy matching)
|
||||
auto sortBySpectrum(const Palette& palette) -> Palette {
|
||||
const size_t N = countActiveColors(palette);
|
||||
std::vector<Uint32> available(palette.begin(), palette.begin() + static_cast<ptrdiff_t>(N));
|
||||
std::vector<Uint32> result;
|
||||
result.reserve(N);
|
||||
|
||||
// Para cada color de referencia del Spectrum, buscar el más cercano disponible
|
||||
const size_t REFS = std::min(N, SPECTRUM_REFERENCE.size());
|
||||
for (size_t i = 0; i < REFS && !available.empty(); ++i) {
|
||||
const Uint32 REF = SPECTRUM_REFERENCE[i];
|
||||
auto best = std::ranges::min_element(available, [REF](Uint32 a, Uint32 b) {
|
||||
return rgbDistanceSq(a, REF) < rgbDistanceSq(b, REF);
|
||||
});
|
||||
result.push_back(*best);
|
||||
available.erase(best);
|
||||
}
|
||||
|
||||
// Si quedan colores sin asignar, añadirlos al final
|
||||
for (const auto& c : available) {
|
||||
result.push_back(c);
|
||||
}
|
||||
|
||||
Palette out{};
|
||||
out.fill(0);
|
||||
std::ranges::copy(result, out.begin());
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ── PaletteManager ───────────────────────────────────────────────────────────
|
||||
|
||||
PaletteManager::PaletteManager(
|
||||
std::vector<std::string> raw_paths,
|
||||
const std::string& initial_name,
|
||||
PaletteSortMode initial_sort_mode,
|
||||
std::shared_ptr<Surface> game_surface,
|
||||
std::shared_ptr<Surface> border_surface,
|
||||
OnChangeCallback on_change)
|
||||
: palettes_(std::move(raw_paths)),
|
||||
sort_mode_(initial_sort_mode),
|
||||
game_surface_(std::move(game_surface)),
|
||||
border_surface_(std::move(border_surface)),
|
||||
on_change_(std::move(on_change)) {
|
||||
current_ = findIndex(initial_name);
|
||||
|
||||
// Leer y aplicar paleta inicial directamente desde el archivo
|
||||
// (Resource::Cache aún no está disponible en este punto del ciclo de vida)
|
||||
const auto INITIAL_PALETTE = sortPalette(readPalFile(palettes_.at(current_)), sort_mode_);
|
||||
game_surface_->setPalette(INITIAL_PALETTE);
|
||||
border_surface_->setPalette(INITIAL_PALETTE);
|
||||
|
||||
// Procesar la lista: conservar solo los nombres de archivo (sin ruta)
|
||||
processPathList();
|
||||
}
|
||||
|
||||
void PaletteManager::next() {
|
||||
if (++current_ == palettes_.size()) {
|
||||
current_ = 0;
|
||||
}
|
||||
apply();
|
||||
}
|
||||
|
||||
void PaletteManager::previous() {
|
||||
current_ = (current_ > 0) ? current_ - 1 : palettes_.size() - 1;
|
||||
apply();
|
||||
}
|
||||
|
||||
auto PaletteManager::setByName(const std::string& name) -> bool {
|
||||
const std::string LOWER_NAME = toLower(name + ".pal");
|
||||
for (size_t i = 0; i < palettes_.size(); ++i) {
|
||||
if (toLower(palettes_[i]) == LOWER_NAME) {
|
||||
current_ = i;
|
||||
apply();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto PaletteManager::getNames() const -> std::vector<std::string> {
|
||||
std::vector<std::string> names;
|
||||
names.reserve(palettes_.size());
|
||||
for (const auto& p : palettes_) {
|
||||
std::string name = p;
|
||||
const size_t POS = name.find(".pal");
|
||||
if (POS != std::string::npos) { name.erase(POS, 4); }
|
||||
std::ranges::transform(name, name.begin(), ::tolower);
|
||||
names.push_back(std::move(name));
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
auto PaletteManager::getCurrentName() const -> std::string {
|
||||
std::string name = palettes_.at(current_);
|
||||
const size_t POS = name.find(".pal");
|
||||
if (POS != std::string::npos) { name.erase(POS, 4); }
|
||||
std::ranges::transform(name, name.begin(), ::tolower);
|
||||
return name;
|
||||
}
|
||||
|
||||
auto PaletteManager::getPrettyName() const -> std::string {
|
||||
std::string name = getCurrentName();
|
||||
std::ranges::replace(name, '-', ' ');
|
||||
return name;
|
||||
}
|
||||
|
||||
void PaletteManager::nextSortMode() {
|
||||
sort_mode_ = static_cast<PaletteSortMode>((static_cast<int>(sort_mode_) + 1) % static_cast<int>(PaletteSortMode::COUNT));
|
||||
Options::video.palette_sort = sortModeToString(sort_mode_);
|
||||
apply();
|
||||
}
|
||||
|
||||
void PaletteManager::setSortMode(PaletteSortMode mode) {
|
||||
sort_mode_ = mode;
|
||||
Options::video.palette_sort = sortModeToString(sort_mode_);
|
||||
apply();
|
||||
}
|
||||
|
||||
auto PaletteManager::getSortMode() const -> PaletteSortMode {
|
||||
return sort_mode_;
|
||||
}
|
||||
|
||||
auto PaletteManager::getSortModeName() const -> std::string {
|
||||
return sortModeToString(sort_mode_);
|
||||
}
|
||||
|
||||
void PaletteManager::apply() {
|
||||
Palette raw = Resource::Cache::get()->getPalette(palettes_.at(current_));
|
||||
Palette sorted = sortPalette(raw, sort_mode_);
|
||||
game_surface_->loadPalette(sorted);
|
||||
border_surface_->loadPalette(sorted);
|
||||
|
||||
Options::video.palette = getCurrentName();
|
||||
|
||||
if (on_change_) {
|
||||
on_change_();
|
||||
}
|
||||
}
|
||||
|
||||
auto PaletteManager::findIndex(const std::string& name) const -> size_t {
|
||||
const std::string LOWER_NAME = toLower(name + ".pal");
|
||||
for (size_t i = 0; i < palettes_.size(); ++i) {
|
||||
if (toLower(getFileName(palettes_[i])) == LOWER_NAME) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// Fallback: buscar la paleta por defecto
|
||||
const std::string DEFAULT_NAME = toLower(std::string(Defaults::Video::PALETTE_NAME) + ".pal");
|
||||
for (size_t i = 0; i < palettes_.size(); ++i) {
|
||||
if (toLower(getFileName(palettes_[i])) == DEFAULT_NAME) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void PaletteManager::processPathList() {
|
||||
for (auto& palette : palettes_) {
|
||||
palette = getFileName(palette);
|
||||
}
|
||||
}
|
||||
|
||||
auto PaletteManager::sortPalette(const Palette& palette, PaletteSortMode mode) -> Palette {
|
||||
switch (mode) {
|
||||
case PaletteSortMode::LUMINANCE:
|
||||
return sortByLuminance(palette);
|
||||
case PaletteSortMode::SPECTRUM:
|
||||
return sortBySpectrum(palette);
|
||||
default:
|
||||
return palette;
|
||||
}
|
||||
}
|
||||
64
source/core/rendering/palette_manager.hpp
Normal file
64
source/core/rendering/palette_manager.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Alias de paleta (igual que en surface.hpp; evita incluir todo el header)
|
||||
using Palette = std::array<Uint32, 256>;
|
||||
|
||||
class Surface;
|
||||
|
||||
// Modo de ordenación de paletas
|
||||
enum class PaletteSortMode : int {
|
||||
ORIGINAL = 0, // Paleta tal cual viene del fichero
|
||||
LUMINANCE = 1, // Ordenada por luminancia percibida
|
||||
SPECTRUM = 2, // Reordenada para imitar la paleta ZX Spectrum
|
||||
COUNT = 3
|
||||
};
|
||||
|
||||
// Conversión string ↔ PaletteSortMode
|
||||
auto sortModeFromString(const std::string& str) -> PaletteSortMode;
|
||||
auto sortModeToString(PaletteSortMode mode) -> std::string;
|
||||
|
||||
class PaletteManager {
|
||||
public:
|
||||
using OnChangeCallback = std::function<void()>;
|
||||
|
||||
PaletteManager(
|
||||
std::vector<std::string> raw_paths,
|
||||
const std::string& initial_name,
|
||||
PaletteSortMode initial_sort_mode,
|
||||
std::shared_ptr<Surface> game_surface,
|
||||
std::shared_ptr<Surface> border_surface,
|
||||
OnChangeCallback on_change = nullptr);
|
||||
|
||||
void next(); // Avanza a la siguiente paleta
|
||||
void previous(); // Retrocede a la paleta anterior
|
||||
auto setByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe
|
||||
[[nodiscard]] auto getNames() const -> std::vector<std::string>; // Nombres disponibles (minúsculas, sin .pal)
|
||||
[[nodiscard]] auto getCurrentName() const -> std::string; // Nombre de la paleta actual (minúsculas, sin .pal)
|
||||
[[nodiscard]] auto getPrettyName() const -> std::string; // Nombre actual con guiones sustituidos por espacios
|
||||
|
||||
void nextSortMode(); // Cicla al siguiente modo de ordenación
|
||||
void setSortMode(PaletteSortMode mode); // Establece un modo de ordenación concreto
|
||||
[[nodiscard]] auto getSortMode() const -> PaletteSortMode; // Devuelve el modo de ordenación actual
|
||||
[[nodiscard]] auto getSortModeName() const -> std::string; // Nombre del modo actual ("ORIGINAL", etc.)
|
||||
|
||||
private:
|
||||
void apply(); // Aplica la paleta actual a ambas surfaces
|
||||
[[nodiscard]] auto findIndex(const std::string& name) const -> size_t; // Localiza paleta por nombre en el vector
|
||||
void processPathList(); // Extrae nombres de archivo de las rutas completas
|
||||
static auto sortPalette(const Palette& palette, PaletteSortMode mode) -> Palette; // Reordena una paleta según el modo
|
||||
|
||||
std::vector<std::string> palettes_;
|
||||
size_t current_{0};
|
||||
PaletteSortMode sort_mode_{PaletteSortMode::ORIGINAL};
|
||||
std::shared_ptr<Surface> game_surface_;
|
||||
std::shared_ptr<Surface> border_surface_;
|
||||
OnChangeCallback on_change_;
|
||||
};
|
||||
107
source/core/rendering/pixel_reveal.cpp
Normal file
107
source/core/rendering/pixel_reveal.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "core/rendering/pixel_reveal.hpp"
|
||||
|
||||
#include <algorithm> // Para min, ranges::all_of
|
||||
#include <numeric> // Para iota
|
||||
#include <queue> // Para queue (BFS en modo ORDERED)
|
||||
#include <random> // Para mt19937, shuffle
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "utils/utils.hpp" // Para PaletteColor
|
||||
|
||||
// Constructor
|
||||
PixelReveal::PixelReveal(int width, int height, float pixels_per_second, float step_duration, int num_steps, bool reverse, RevealMode mode)
|
||||
: cover_surface_(std::make_shared<Surface>(width, height)),
|
||||
reveal_order_(height),
|
||||
row_step_(height, 0),
|
||||
width_(width),
|
||||
height_(height),
|
||||
pixels_per_second_(pixels_per_second),
|
||||
step_duration_(step_duration),
|
||||
num_steps_(num_steps),
|
||||
reverse_(reverse),
|
||||
mode_(mode) {
|
||||
// En modo normal: empieza negro sólido (se irá revelando a transparente)
|
||||
// En modo inverso: empieza transparente (se irá cubriendo de negro)
|
||||
const auto INITIAL_COLOR = reverse_ ? static_cast<Uint8>(PaletteColor::TRANSPARENT) : static_cast<Uint8>(PaletteColor::BLACK);
|
||||
cover_surface_->clear(INITIAL_COLOR);
|
||||
|
||||
if (mode_ == RevealMode::ORDERED) {
|
||||
// Calcula offsets por bisección BFS: 0, N/2, N/4, 3N/4, ...
|
||||
std::vector<int> offsets;
|
||||
offsets.push_back(0);
|
||||
std::queue<std::pair<int, int>> bq;
|
||||
bq.emplace(0, num_steps_);
|
||||
while (static_cast<int>(offsets.size()) < num_steps_) {
|
||||
auto [lo, hi] = bq.front();
|
||||
bq.pop();
|
||||
if (hi - lo <= 1) {
|
||||
continue;
|
||||
}
|
||||
const int MID = (lo + hi) / 2;
|
||||
offsets.push_back(MID);
|
||||
bq.emplace(lo, MID);
|
||||
bq.emplace(MID, hi);
|
||||
}
|
||||
// Genera el orden: para cada offset, todas las columnas col = offset, offset+N, offset+2N, ...
|
||||
std::vector<int> ordered_cols;
|
||||
ordered_cols.reserve(width_);
|
||||
for (const int OFF : offsets) {
|
||||
for (int col = OFF; col < width_; col += num_steps_) {
|
||||
ordered_cols.push_back(col);
|
||||
}
|
||||
}
|
||||
// Todas las filas usan el mismo orden (sin aleatoriedad)
|
||||
for (int r = 0; r < height_; r++) {
|
||||
reveal_order_[r] = ordered_cols;
|
||||
}
|
||||
} else {
|
||||
// Modo RANDOM: orden aleatorio por fila usando la fila como semilla (reproducible)
|
||||
for (int r = 0; r < height_; r++) {
|
||||
reveal_order_[r].resize(width_);
|
||||
std::iota(reveal_order_[r].begin(), reveal_order_[r].end(), 0);
|
||||
std::mt19937 rng(static_cast<unsigned int>(r));
|
||||
std::shuffle(reveal_order_[r].begin(), reveal_order_[r].end(), rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actualiza el estado del revelado
|
||||
void PixelReveal::update(float time_active) { // NOLINT(readability-make-member-function-const)
|
||||
// En modo normal revela (pone transparente); en modo inverso cubre (pone negro)
|
||||
const auto PIXEL_COLOR = reverse_ ? static_cast<Uint8>(PaletteColor::BLACK) : static_cast<Uint8>(PaletteColor::TRANSPARENT);
|
||||
|
||||
for (int r = 0; r < height_; r++) {
|
||||
const float T_START = static_cast<float>(r) / pixels_per_second_;
|
||||
const float TIME_IN_ROW = time_active - T_START;
|
||||
|
||||
if (TIME_IN_ROW < 0.0F) {
|
||||
continue; // Esta fila aún no ha empezado
|
||||
}
|
||||
|
||||
const int STEPS = std::min(num_steps_, static_cast<int>(TIME_IN_ROW / step_duration_));
|
||||
|
||||
if (STEPS > row_step_[r]) {
|
||||
// Procesa los píxeles de los pasos pendientes
|
||||
for (int step = row_step_[r]; step < STEPS; step++) {
|
||||
const int START_IDX = step * width_ / num_steps_;
|
||||
const int END_IDX = (step == num_steps_ - 1) ? width_ : (step + 1) * width_ / num_steps_;
|
||||
|
||||
for (int idx = START_IDX; idx < END_IDX; idx++) {
|
||||
const int COL = reveal_order_[r][idx];
|
||||
cover_surface_->putPixel(COL, r, PIXEL_COLOR);
|
||||
}
|
||||
}
|
||||
row_step_[r] = STEPS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja la máscara en la posición indicada
|
||||
void PixelReveal::render(int dst_x, int dst_y) const {
|
||||
cover_surface_->render(dst_x, dst_y);
|
||||
}
|
||||
|
||||
// Indica si el revelado ha completado todas las filas
|
||||
auto PixelReveal::isComplete() const -> bool {
|
||||
return std::ranges::all_of(row_step_, [this](int s) -> bool { return s >= num_steps_; });
|
||||
}
|
||||
41
source/core/rendering/pixel_reveal.hpp
Normal file
41
source/core/rendering/pixel_reveal.hpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <vector> // Para vector
|
||||
|
||||
class Surface;
|
||||
|
||||
// Efecto de revelado pixel a pixel por filas, de arriba a abajo.
|
||||
// Cada fila se revela en num_steps pasos, con píxeles en orden aleatorio u ordenado (bisección).
|
||||
class PixelReveal {
|
||||
public:
|
||||
// Modo de revelado: aleatorio por fila o en orden de bisección (dithering ordenado 1D)
|
||||
enum class RevealMode { RANDOM,
|
||||
ORDERED };
|
||||
|
||||
// Constructor
|
||||
PixelReveal(int width, int height, float pixels_per_second, float step_duration, int num_steps = 4, bool reverse = false, RevealMode mode = RevealMode::RANDOM);
|
||||
|
||||
~PixelReveal() = default;
|
||||
|
||||
// Actualiza el estado del revelado según el tiempo transcurrido
|
||||
void update(float time_active);
|
||||
|
||||
// Dibuja la máscara de revelado en la posición indicada
|
||||
void render(int dst_x, int dst_y) const;
|
||||
|
||||
// Indica si el revelado ha completado todas las filas
|
||||
[[nodiscard]] auto isComplete() const -> bool;
|
||||
|
||||
private:
|
||||
std::shared_ptr<Surface> cover_surface_; // Máscara negra que se va haciendo transparente
|
||||
std::vector<std::vector<int>> reveal_order_; // Orden de columnas por fila (aleatorio u ordenado por bisección)
|
||||
std::vector<int> row_step_; // Paso actual de revelado por fila (0..num_steps_)
|
||||
int width_;
|
||||
int height_;
|
||||
float pixels_per_second_; // Filas reveladas por segundo
|
||||
float step_duration_; // Segundos por paso dentro de una fila
|
||||
int num_steps_; // Número de pasos de revelado por fila
|
||||
bool reverse_; // Si true: transparente → negro (ocultar); si false: negro → transparente (revelar)
|
||||
RevealMode mode_; // Modo de revelado: aleatorio u ordenado por bisección
|
||||
};
|
||||
159
source/core/rendering/render_info.cpp
Normal file
159
source/core/rendering/render_info.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
#include "core/rendering/render_info.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para transform
|
||||
#include <cmath> // Para round, floor
|
||||
#include <iomanip> // Para setprecision
|
||||
#include <sstream> // Para ostringstream
|
||||
#include <string> // Para string
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "game/options.hpp" // Para Options
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "utils/utils.hpp" // Para prettyName
|
||||
|
||||
// [SINGLETON]
|
||||
RenderInfo* RenderInfo::render_info = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||
void RenderInfo::init() {
|
||||
RenderInfo::render_info = new RenderInfo();
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
void RenderInfo::destroy() {
|
||||
delete RenderInfo::render_info;
|
||||
RenderInfo::render_info = nullptr;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto RenderInfo::get() -> RenderInfo* {
|
||||
return RenderInfo::render_info;
|
||||
}
|
||||
|
||||
// Constructor: en DEBUG se activa inmediatamente (notifica a Notifier del offset)
|
||||
RenderInfo::RenderInfo() {
|
||||
#ifdef _DEBUG
|
||||
toggle();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Actualiza la animación de entrada/salida del overlay
|
||||
void RenderInfo::update(float delta_time) {
|
||||
switch (status_) {
|
||||
case Status::RISING:
|
||||
y_ += SLIDE_SPEED * delta_time;
|
||||
if (y_ >= 0.0F) {
|
||||
y_ = 0.0F;
|
||||
status_ = Status::ACTIVE;
|
||||
}
|
||||
break;
|
||||
case Status::VANISHING:
|
||||
y_ -= SLIDE_SPEED * delta_time;
|
||||
if (y_ <= static_cast<float>(-HEIGHT)) {
|
||||
y_ = static_cast<float>(-HEIGHT);
|
||||
status_ = Status::HIDDEN;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Renderiza el overlay de información por pantalla
|
||||
void RenderInfo::render() const {
|
||||
if (status_ == Status::HIDDEN) { return; }
|
||||
|
||||
// FPS
|
||||
std::string line = std::to_string(Screen::get()->getLastFPS()) + " fps";
|
||||
|
||||
// Driver GPU
|
||||
const auto& driver = Screen::get()->getGPUDriver();
|
||||
line += " | " + (driver.empty() ? std::string("sdl") : driver);
|
||||
|
||||
// Zoom calculado (alto físico / alto lógico), con coma decimal y sin ceros innecesarios
|
||||
const float ROUNDED = std::round(Screen::get()->getZoomFactor() * 100.0F) / 100.0F;
|
||||
std::string zoom_str;
|
||||
if (ROUNDED == std::floor(ROUNDED)) {
|
||||
zoom_str = std::to_string(static_cast<int>(ROUNDED));
|
||||
} else {
|
||||
std::ostringstream oss;
|
||||
oss << std::fixed << std::setprecision(2) << ROUNDED;
|
||||
zoom_str = oss.str();
|
||||
if (zoom_str.back() == '0') { zoom_str.pop_back(); }
|
||||
std::ranges::replace(zoom_str, '.', ',');
|
||||
}
|
||||
line += " | " + zoom_str + "x";
|
||||
|
||||
// PostFX: muestra shader + preset y supersampling, o nada si está desactivado
|
||||
if (Options::video.shader.enabled) {
|
||||
const bool IS_CRTPI = (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI);
|
||||
const std::string SHADER_NAME = IS_CRTPI ? "crtpi" : "postfx";
|
||||
std::string preset_name = "-";
|
||||
if (IS_CRTPI) {
|
||||
if (!Options::crtpi_presets.empty()) {
|
||||
preset_name = prettyName(Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)].name);
|
||||
}
|
||||
} else {
|
||||
if (!Options::postfx_presets.empty()) {
|
||||
preset_name = prettyName(Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)].name);
|
||||
}
|
||||
}
|
||||
const bool SHOW_SS = Options::video.supersampling.enabled && !IS_CRTPI;
|
||||
line += " | " + SHADER_NAME + " " + preset_name + (SHOW_SS ? " (ss)" : "");
|
||||
}
|
||||
|
||||
// Todo en lowercase
|
||||
std::ranges::transform(line, line.begin(), [](unsigned char c) { return std::tolower(c); });
|
||||
|
||||
// Constantes visuales (igual que Console)
|
||||
static constexpr Uint8 BG_COLOR = 0; // PaletteColor::BLACK
|
||||
static constexpr Uint8 MSG_COLOR = 9; // PaletteColor::BRIGHT_GREEN
|
||||
static constexpr int TEXT_SIZE = 6;
|
||||
static constexpr int PADDING_V = (TEXT_SIZE / 2) - 1;
|
||||
|
||||
// Fuente: preferir la de la consola si está disponible
|
||||
auto text_obj = (Console::get() != nullptr) ? Console::get()->getText() : Screen::get()->getText();
|
||||
|
||||
// Posición Y: debajo de la consola + offset animado propio
|
||||
const int CONSOLE_Y = (Console::get() != nullptr) ? Console::get()->getVisibleHeight() : 0;
|
||||
const int Y = CONSOLE_Y + static_cast<int>(y_);
|
||||
|
||||
// Rectángulo de fondo: ancho completo, alto ajustado al texto
|
||||
const SDL_FRect RECT = {
|
||||
.x = 0.0F,
|
||||
.y = static_cast<float>(Y),
|
||||
.w = Options::game.width,
|
||||
.h = static_cast<float>(TEXT_SIZE + (PADDING_V * 2))};
|
||||
|
||||
auto game_surface = Screen::get()->getGameSurface();
|
||||
game_surface->fillRect(&RECT, BG_COLOR);
|
||||
// game_surface->drawRectBorder(&RECT, BORDER_COLOR);
|
||||
text_obj->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
static_cast<int>(Options::game.width / 2),
|
||||
Y + PADDING_V,
|
||||
line,
|
||||
1,
|
||||
MSG_COLOR);
|
||||
}
|
||||
|
||||
// Activa o desactiva el overlay y notifica a Notifier del cambio de offset
|
||||
void RenderInfo::toggle() {
|
||||
switch (status_) {
|
||||
case Status::HIDDEN:
|
||||
status_ = Status::RISING;
|
||||
Screen::get()->updateZoomFactor();
|
||||
if (Notifier::get() != nullptr) { Notifier::get()->addYOffset(HEIGHT); }
|
||||
break;
|
||||
case Status::ACTIVE:
|
||||
status_ = Status::VANISHING;
|
||||
if (Notifier::get() != nullptr) { Notifier::get()->removeYOffset(HEIGHT); }
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
37
source/core/rendering/render_info.hpp
Normal file
37
source/core/rendering/render_info.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
class RenderInfo {
|
||||
public:
|
||||
// Singleton
|
||||
static void init();
|
||||
static void destroy();
|
||||
static auto get() -> RenderInfo*;
|
||||
|
||||
// Métodos principales
|
||||
void update(float delta_time);
|
||||
void render() const;
|
||||
void toggle();
|
||||
|
||||
// Consultas
|
||||
[[nodiscard]] auto isActive() const -> bool { return status_ != Status::HIDDEN; }
|
||||
|
||||
// Altura fija del overlay (TEXT_SIZE(6) + PADDING_V(2) * 2)
|
||||
static constexpr int HEIGHT = 10;
|
||||
static constexpr float SLIDE_SPEED = 120.0F;
|
||||
|
||||
private:
|
||||
enum class Status { HIDDEN,
|
||||
RISING,
|
||||
ACTIVE,
|
||||
VANISHING };
|
||||
|
||||
// Singleton
|
||||
static RenderInfo* render_info;
|
||||
|
||||
// Constructor y destructor privados [SINGLETON]
|
||||
RenderInfo();
|
||||
~RenderInfo() = default;
|
||||
|
||||
Status status_{Status::HIDDEN};
|
||||
float y_{static_cast<float>(-HEIGHT)};
|
||||
};
|
||||
750
source/core/rendering/screen.cpp
Normal file
750
source/core/rendering/screen.cpp
Normal file
@@ -0,0 +1,750 @@
|
||||
#include "core/rendering/screen.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para max, min, transform
|
||||
#include <cctype> // Para toupper
|
||||
#include <cmath> // Para round, floor
|
||||
#include <cstring> // Para memcpy
|
||||
#include <fstream> // Para basic_ostream, operator<<, endl, basic_...
|
||||
#include <iostream> // Para cerr
|
||||
#include <iterator> // Para istreambuf_iterator, operator==
|
||||
#include <string> // Para char_traits, string, operator+, operator==
|
||||
|
||||
#include "core/input/mouse.hpp" // Para updateCursorVisibility
|
||||
#include "core/rendering/render_info.hpp" // Para RenderInfo
|
||||
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
|
||||
#include "core/rendering/surface.hpp" // Para Surface, readPalFile
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "core/resources/resource_list.hpp" // Para Asset, AssetType
|
||||
#include "game/options.hpp" // Para Options, options, OptionsVideo, Border
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
|
||||
// [SINGLETON]
|
||||
Screen* Screen::screen = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||
void Screen::init() {
|
||||
Screen::screen = new Screen();
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
void Screen::destroy() {
|
||||
delete Screen::screen;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto Screen::get() -> Screen* {
|
||||
return Screen::screen;
|
||||
}
|
||||
|
||||
// Constructor
|
||||
Screen::Screen() {
|
||||
// Arranca SDL VIDEO, crea la ventana y el renderizador
|
||||
initSDLVideo();
|
||||
if (Options::video.fullscreen) { SDL_HideCursor(); }
|
||||
|
||||
// Calcular tamaños y hacer .resize() de los buffers de píxeles
|
||||
adjustWindowSize();
|
||||
adjustRenderLogicalSize();
|
||||
updateZoomFactor();
|
||||
|
||||
// Ajusta los tamaños
|
||||
game_surface_dstrect_ = {.x = Options::video.border.width, .y = Options::video.border.height, .w = Options::game.width, .h = Options::game.height};
|
||||
|
||||
// Define el color del borde para el modo de pantalla completa
|
||||
border_color_ = static_cast<Uint8>(PaletteColor::BLACK);
|
||||
|
||||
// Crea la textura donde se dibujan los graficos del juego
|
||||
game_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, Options::game.width, Options::game.height);
|
||||
if (game_texture_ == nullptr) {
|
||||
// Registrar el error si está habilitado
|
||||
std::cerr << "Error: game_texture_ could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||
}
|
||||
SDL_SetTextureScaleMode(game_texture_, SDL_SCALEMODE_NEAREST);
|
||||
|
||||
// Crea la textura donde se dibuja el borde que rodea el area de juego
|
||||
border_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, Options::game.width + (Options::video.border.width * 2), Options::game.height + (Options::video.border.height * 2));
|
||||
if (border_texture_ == nullptr) {
|
||||
// Registrar el error si está habilitado
|
||||
std::cerr << "Error: border_texture_ could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||
}
|
||||
SDL_SetTextureScaleMode(border_texture_, SDL_SCALEMODE_NEAREST);
|
||||
|
||||
// Crea las surfaces (PaletteManager aplicará la paleta inicial en su constructor)
|
||||
game_surface_ = std::make_shared<Surface>(Options::game.width, Options::game.height);
|
||||
game_surface_->clear(static_cast<Uint8>(PaletteColor::BLACK));
|
||||
|
||||
border_surface_ = std::make_shared<Surface>(Options::game.width + (Options::video.border.width * 2), Options::game.height + (Options::video.border.height * 2));
|
||||
border_surface_->clear(border_color_);
|
||||
|
||||
// Crea el gestor de paletas; aplica la paleta inicial a ambas surfaces
|
||||
palette_manager_ = std::make_unique<PaletteManager>(
|
||||
Resource::List::get()->getListByType(Resource::List::Type::PALETTE),
|
||||
Options::video.palette,
|
||||
sortModeFromString(Options::video.palette_sort),
|
||||
game_surface_,
|
||||
border_surface_,
|
||||
[this]() {
|
||||
// Actualizar caché ARGB del borde cuando cambia la paleta
|
||||
if (border_is_solid_) {
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
border_argb_color_ = border_pixel_buffer_[0];
|
||||
}
|
||||
});
|
||||
|
||||
// Cachear el color ARGB inicial del borde (borde sólido por defecto)
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
border_argb_color_ = border_pixel_buffer_[0];
|
||||
|
||||
// Establece la surface que actuará como renderer para recibir las llamadas a render()
|
||||
renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(game_surface_);
|
||||
|
||||
// Crea el objeto de texto para la pantalla de carga
|
||||
createText();
|
||||
|
||||
// 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_texture_, nullptr, nullptr);
|
||||
SDL_RenderTexture(renderer_, border_texture_, nullptr, nullptr);
|
||||
|
||||
// Ahora sí inicializar los shaders
|
||||
initShaders();
|
||||
}
|
||||
|
||||
// Destructor
|
||||
Screen::~Screen() {
|
||||
SDL_DestroyTexture(game_texture_);
|
||||
SDL_DestroyTexture(border_texture_);
|
||||
}
|
||||
|
||||
// Limpia el renderer
|
||||
void Screen::clearRenderer(Rgb 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() { setRendererSurface(nullptr); }
|
||||
|
||||
// Vuelca el contenido del renderizador en pantalla
|
||||
void Screen::render() {
|
||||
fps_.increment();
|
||||
|
||||
// Renderiza todos los overlays (escribe en game_surface_ CPU-side)
|
||||
renderOverlays();
|
||||
|
||||
// En el path SDL3GPU, los píxeles se suben directamente desde la Surface.
|
||||
// En el path SDL_Renderer, primero copiamos la surface a la SDL_Texture.
|
||||
if (!(shader_backend_ && shader_backend_->isHardwareAccelerated())) {
|
||||
surfaceToTexture();
|
||||
}
|
||||
|
||||
// Copia la textura al renderizador (o hace el present GPU)
|
||||
textureToRenderer();
|
||||
}
|
||||
|
||||
// Establece el modo de video
|
||||
void Screen::setVideoMode(bool mode) {
|
||||
// Actualiza las opciones
|
||||
Options::video.fullscreen = mode;
|
||||
|
||||
// Configura el modo de pantalla y ajusta la ventana
|
||||
SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
|
||||
SDL_SyncWindow(window_);
|
||||
adjustWindowSize();
|
||||
adjustRenderLogicalSize();
|
||||
updateZoomFactor();
|
||||
}
|
||||
|
||||
// Camibia entre pantalla completa y ventana
|
||||
void Screen::toggleVideoMode() {
|
||||
Options::video.fullscreen = !Options::video.fullscreen;
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
}
|
||||
|
||||
// Reduce el tamaño de la ventana
|
||||
auto Screen::decWindowZoom() -> bool {
|
||||
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||
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) {
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Aumenta el tamaño de la ventana
|
||||
auto Screen::incWindowZoom() -> bool {
|
||||
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||
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) {
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Establece el zoom directamente; false si fuera del rango [1, max_zoom] o en pantalla completa
|
||||
auto Screen::setWindowZoom(int zoom) -> bool {
|
||||
if (Options::video.fullscreen) { return false; }
|
||||
if (zoom < 1 || zoom > Options::window.max_zoom) { return false; }
|
||||
if (zoom == Options::window.zoom) { return false; }
|
||||
Options::window.zoom = zoom;
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Devuelve el zoom máximo permitido según la pantalla actual
|
||||
auto Screen::getMaxZoom() -> int {
|
||||
return Options::window.max_zoom;
|
||||
}
|
||||
|
||||
// Cambia el color del borde
|
||||
void Screen::setBorderColor(Uint8 color) {
|
||||
border_color_ = color;
|
||||
border_surface_->clear(border_color_);
|
||||
|
||||
// Actualizar caché ARGB del borde sólido (ocurre una vez por habitación, no cada frame)
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
border_argb_color_ = border_pixel_buffer_[0];
|
||||
border_is_solid_ = true;
|
||||
}
|
||||
|
||||
// Cambia entre borde visible y no visible
|
||||
void Screen::toggleBorder() {
|
||||
Options::video.border.enabled = !Options::video.border.enabled;
|
||||
setVideoMode(Options::video.fullscreen);
|
||||
initShaders();
|
||||
}
|
||||
|
||||
// Dibuja las notificaciones
|
||||
void Screen::renderNotifications() const {
|
||||
if (notifications_enabled_) {
|
||||
Notifier::get()->render();
|
||||
}
|
||||
}
|
||||
|
||||
// Activa/desactiva todos los shaders respetando el shader actualmente seleccionado
|
||||
void Screen::toggleShaders() {
|
||||
Options::video.shader.enabled = !Options::video.shader.enabled;
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
if (Options::video.shader.enabled) {
|
||||
// Activar: usar el shader actualmente seleccionado
|
||||
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
||||
applyCurrentCrtPiPreset();
|
||||
} else {
|
||||
applyCurrentPostFXPreset();
|
||||
}
|
||||
} else {
|
||||
// Desactivar: pass-through con POSTFX (pipeline sin efecto)
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
|
||||
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||
}
|
||||
} else {
|
||||
// Backend no inicializado aún — inicializarlo ahora
|
||||
initShaders();
|
||||
}
|
||||
}
|
||||
|
||||
// Recarga el shader del preset actual sin toggle
|
||||
void Screen::reloadPostFX() {
|
||||
if (Options::video.shader.enabled && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
// El backend ya está activo: solo actualizar uniforms, sin recrear el pipeline
|
||||
applyCurrentPostFXPreset();
|
||||
} else if (Options::video.shader.enabled) {
|
||||
initShaders();
|
||||
}
|
||||
}
|
||||
|
||||
// Recarga el shader CrtPi del preset actual sin toggle
|
||||
void Screen::reloadCrtPi() {
|
||||
if (!shader_backend_) { return; }
|
||||
applyCurrentCrtPiPreset();
|
||||
}
|
||||
|
||||
// Actualiza la lógica de la clase (versión nueva con delta_time para escenas migradas)
|
||||
void Screen::update(float delta_time) {
|
||||
fps_.calculate(SDL_GetTicks());
|
||||
Notifier::get()->update(delta_time);
|
||||
if (Console::get() != nullptr) {
|
||||
Console::get()->update(delta_time);
|
||||
}
|
||||
if (RenderInfo::get() != nullptr) { RenderInfo::get()->update(delta_time); }
|
||||
Mouse::updateCursorVisibility();
|
||||
}
|
||||
|
||||
// Calcula el tamaño de la ventana
|
||||
void Screen::adjustWindowSize() {
|
||||
window_width_ = Options::game.width + (Options::video.border.enabled ? Options::video.border.width * 2 : 0);
|
||||
window_height_ = Options::game.height + (Options::video.border.enabled ? Options::video.border.height * 2 : 0);
|
||||
|
||||
// Reservamos memoria una sola vez.
|
||||
// Si el buffer es más pequeño que la superficie, crash asegurado.
|
||||
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_));
|
||||
game_pixel_buffer_.resize(static_cast<size_t>(Options::game.width * Options::game.height));
|
||||
|
||||
// border_pixel_buffer_ es el buffer que se sube a la GPU (tamaño total ventana).
|
||||
if (Options::video.border.enabled) {
|
||||
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_));
|
||||
}
|
||||
|
||||
// Lógica de centrado y redimensionado de ventana SDL
|
||||
if (static_cast<int>(Options::video.fullscreen) == 0) {
|
||||
int old_w;
|
||||
int old_h;
|
||||
SDL_GetWindowSize(window_, &old_w, &old_h);
|
||||
int old_x;
|
||||
int old_y;
|
||||
SDL_GetWindowPosition(window_, &old_x, &old_y);
|
||||
|
||||
const int NEW_W = window_width_ * Options::window.zoom;
|
||||
const int NEW_H = window_height_ * Options::window.zoom;
|
||||
const int NEW_X = old_x + ((old_w - NEW_W) / 2);
|
||||
const int NEW_Y = old_y + ((old_h - NEW_H) / 2);
|
||||
|
||||
SDL_SetWindowSize(window_, NEW_W, NEW_H);
|
||||
|
||||
// En Wayland, SDL_SetWindowPosition es ignorado por el compositor (limitación de
|
||||
// protocolo: el compositor controla la posición de ventanas toplevel). Solo se
|
||||
// aplica en X11/Windows/macOS donde el posicionado funciona correctamente.
|
||||
// SDL_SyncWindow garantiza que el resize esté completado antes de reposicionar
|
||||
// (evita el race condition en X11).
|
||||
SDL_SyncWindow(window_);
|
||||
const char* driver = SDL_GetCurrentVideoDriver();
|
||||
const bool IS_WAYLAND = (driver != nullptr && SDL_strcmp(driver, "wayland") == 0);
|
||||
if (!IS_WAYLAND) {
|
||||
SDL_SetWindowPosition(window_, std::max(NEW_X, WINDOWS_DECORATIONS), std::max(NEW_Y, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ajusta el tamaño lógico del renderizador
|
||||
void Screen::adjustRenderLogicalSize() {
|
||||
SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
}
|
||||
|
||||
// Recalcula y almacena el factor de zoom. Llamar solo cuando SDL ya ha estabilizado el estado de la ventana.
|
||||
// En ventana: Options::window.zoom (siempre entero).
|
||||
// En fullscreen: mínimo de las escalas en ambos ejes; floor si integer scale está activo.
|
||||
void Screen::updateZoomFactor() {
|
||||
if (!Options::video.fullscreen) {
|
||||
zoom_factor_ = static_cast<float>(Options::window.zoom);
|
||||
return;
|
||||
}
|
||||
if (window_width_ == 0 || window_height_ == 0) {
|
||||
zoom_factor_ = 1.0F;
|
||||
return;
|
||||
}
|
||||
int pw{0};
|
||||
int ph{0};
|
||||
SDL_GetRenderOutputSize(renderer_, &pw, &ph);
|
||||
const float SCALE = std::min(static_cast<float>(pw) / static_cast<float>(window_width_),
|
||||
static_cast<float>(ph) / static_cast<float>(window_height_));
|
||||
zoom_factor_ = Options::video.integer_scale ? std::floor(SCALE) : SCALE;
|
||||
}
|
||||
|
||||
// Establece el renderizador para las surfaces
|
||||
void Screen::setRendererSurface(const std::shared_ptr<Surface>& surface) {
|
||||
(surface) ? renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(surface) : renderer_surface_ = std::make_shared<std::shared_ptr<Surface>>(game_surface_);
|
||||
}
|
||||
|
||||
// Cambia la paleta
|
||||
void Screen::nextPalette() { palette_manager_->next(); }
|
||||
|
||||
// Cambia la paleta
|
||||
void Screen::previousPalette() { palette_manager_->previous(); }
|
||||
|
||||
// Copia la surface a la textura
|
||||
void Screen::surfaceToTexture() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (Options::video.border.enabled) {
|
||||
border_surface_->copyToTexture(renderer_, border_texture_);
|
||||
game_surface_->copyToTexture(renderer_, border_texture_, nullptr, &game_surface_dstrect_);
|
||||
} else {
|
||||
game_surface_->copyToTexture(renderer_, game_texture_);
|
||||
}
|
||||
}
|
||||
|
||||
// Copia la textura al renderizador (o hace el present GPU)
|
||||
void Screen::textureToRenderer() {
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
const int GAME_W = Options::game.width;
|
||||
const int GAME_H = Options::game.height;
|
||||
|
||||
if (Options::video.border.enabled) {
|
||||
const int BORDER_W = window_width_;
|
||||
const int BORDER_H = window_height_;
|
||||
const int OFF_X = static_cast<int>(game_surface_dstrect_.x);
|
||||
const int OFF_Y = static_cast<int>(game_surface_dstrect_.y);
|
||||
|
||||
if (border_is_solid_) {
|
||||
// Path A: borde sólido (gameplay normal)
|
||||
// Rellena solo el marco con el color cacheado — sin lookups de paleta.
|
||||
// El área central (juego) se deja sin tocar; el overlay la sobreescribe igualmente.
|
||||
|
||||
// Franjas superior e inferior (ancho completo)
|
||||
std::fill_n(border_pixel_buffer_.data(), OFF_Y * BORDER_W, border_argb_color_);
|
||||
std::fill_n(&border_pixel_buffer_[(OFF_Y + GAME_H) * BORDER_W],
|
||||
(BORDER_H - OFF_Y - GAME_H) * BORDER_W,
|
||||
border_argb_color_);
|
||||
// Columnas laterales en las filas del área de juego
|
||||
for (int y = OFF_Y; y < OFF_Y + GAME_H; ++y) {
|
||||
std::fill_n(&border_pixel_buffer_[y * BORDER_W], OFF_X, border_argb_color_);
|
||||
std::fill_n(&border_pixel_buffer_[(y * BORDER_W) + OFF_X + GAME_W],
|
||||
BORDER_W - OFF_X - GAME_W,
|
||||
border_argb_color_);
|
||||
}
|
||||
} else {
|
||||
// Path B: borde dinámico (escena de carga — bandas de colores animadas)
|
||||
// Conversión completa: la escena modifica border_surface_ cada frame
|
||||
border_surface_->toARGBBuffer(border_pixel_buffer_.data());
|
||||
}
|
||||
|
||||
// Overlay del juego sobre el centro del buffer (ambos paths)
|
||||
game_surface_->toARGBBuffer(game_pixel_buffer_.data());
|
||||
for (int y = 0; y < GAME_H; ++y) {
|
||||
const Uint32* src = &game_pixel_buffer_[y * GAME_W];
|
||||
Uint32* dst = &border_pixel_buffer_[((OFF_Y + y) * BORDER_W) + OFF_X];
|
||||
std::memcpy(dst, src, GAME_W * sizeof(Uint32));
|
||||
}
|
||||
|
||||
shader_backend_->uploadPixels(border_pixel_buffer_.data(), BORDER_W, BORDER_H);
|
||||
} else {
|
||||
// Caso sin borde: subida directa simplificada
|
||||
game_surface_->toARGBBuffer(game_pixel_buffer_.data());
|
||||
shader_backend_->uploadPixels(game_pixel_buffer_.data(), GAME_W, GAME_H);
|
||||
}
|
||||
|
||||
shader_backend_->render();
|
||||
} else {
|
||||
// Fallback SDL_Renderer (mantiene tu lógica de texturas SDL)
|
||||
SDL_Texture* tex = Options::video.border.enabled ? border_texture_ : game_texture_;
|
||||
SDL_SetRenderTarget(renderer_, nullptr);
|
||||
SDL_RenderClear(renderer_);
|
||||
SDL_RenderTexture(renderer_, tex, nullptr, nullptr);
|
||||
SDL_RenderPresent(renderer_);
|
||||
}
|
||||
}
|
||||
|
||||
// Renderiza todos los overlays (orden: último dibujado queda encima)
|
||||
void Screen::renderOverlays() {
|
||||
renderNotifications(); // Notifier (abajo)
|
||||
if (RenderInfo::get() != nullptr) { RenderInfo::get()->render(); } // RenderInfo (medio)
|
||||
if (Console::get() != nullptr) { Console::get()->render(); } // Console (encima)
|
||||
}
|
||||
|
||||
// Cambia a una paleta por nombre (case-insensitive); devuelve false si no existe
|
||||
auto Screen::setPaletteByName(const std::string& name) -> bool { return palette_manager_->setByName(name); }
|
||||
|
||||
// Devuelve los nombres de paletas disponibles (minúsculas, sin extensión .pal)
|
||||
auto Screen::getPaletteNames() const -> std::vector<std::string> { return palette_manager_->getNames(); }
|
||||
auto Screen::getPalettePrettyName() const -> std::string { return palette_manager_->getPrettyName(); }
|
||||
void Screen::nextPaletteSortMode() { palette_manager_->nextSortMode(); }
|
||||
void Screen::setPaletteSortMode(PaletteSortMode mode) { palette_manager_->setSortMode(mode); }
|
||||
auto Screen::getPaletteSortModeName() const -> std::string { return palette_manager_->getSortModeName(); }
|
||||
|
||||
// Limpia la game_surface_
|
||||
void Screen::clearSurface(Uint8 index) { game_surface_->clear(index); }
|
||||
|
||||
// Establece el tamaño del borde
|
||||
void Screen::setBorderWidth(int width) { Options::video.border.width = width; }
|
||||
|
||||
// Establece el tamaño del borde
|
||||
void Screen::setBorderHeight(int height) { Options::video.border.height = height; }
|
||||
|
||||
// Establece si se ha de ver el borde en el modo ventana
|
||||
void Screen::setBorderEnabled(bool value) { Options::video.border.enabled = value; }
|
||||
|
||||
// Muestra la ventana
|
||||
void Screen::show() { SDL_ShowWindow(window_); }
|
||||
|
||||
// Oculta la ventana
|
||||
void Screen::hide() { SDL_HideWindow(window_); }
|
||||
|
||||
// Establece la visibilidad de las notificaciones
|
||||
void Screen::setNotificationsEnabled(bool value) { notifications_enabled_ = value; }
|
||||
|
||||
// Alterna entre activar y desactivar el escalado entero
|
||||
void Screen::toggleIntegerScale() {
|
||||
Options::video.integer_scale = !Options::video.integer_scale;
|
||||
SDL_SetRenderLogicalPresentation(renderer_, Options::game.width, Options::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);
|
||||
}
|
||||
updateZoomFactor();
|
||||
}
|
||||
|
||||
// Alterna entre activar y desactivar el V-Sync
|
||||
void Screen::toggleVSync() {
|
||||
Options::video.vertical_sync = !Options::video.vertical_sync;
|
||||
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||
if (shader_backend_) {
|
||||
shader_backend_->setVSync(Options::video.vertical_sync);
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
auto Screen::getRenderer() -> SDL_Renderer* { return renderer_; }
|
||||
auto Screen::getRendererSurface() -> std::shared_ptr<Surface> { return (*renderer_surface_); }
|
||||
auto Screen::getGameSurface() -> std::shared_ptr<Surface> { return game_surface_; }
|
||||
auto Screen::getBorderSurface() -> std::shared_ptr<Surface> {
|
||||
border_is_solid_ = false; // Modificación externa → modo borde dinámico
|
||||
return border_surface_;
|
||||
}
|
||||
|
||||
auto loadData(const std::string& filepath) -> std::vector<uint8_t> {
|
||||
// Load using ResourceHelper (supports both filesystem and pack)
|
||||
return Resource::Helper::loadFile(filepath);
|
||||
}
|
||||
|
||||
void Screen::setLinearUpscale(bool linear) {
|
||||
Options::video.supersampling.linear_upscale = linear;
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
shader_backend_->setLinearUpscale(linear);
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::setDownscaleAlgo(int algo) {
|
||||
Options::video.supersampling.downscale_algo = algo;
|
||||
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
shader_backend_->setDownscaleAlgo(algo);
|
||||
}
|
||||
}
|
||||
|
||||
auto Screen::getSsTextureSize() const -> std::pair<int, int> {
|
||||
if (!shader_backend_) { return {0, 0}; }
|
||||
return shader_backend_->getSsTextureSize();
|
||||
}
|
||||
|
||||
// Activa/desactiva el supersampling global (Ctrl+F4)
|
||||
void Screen::toggleSupersampling() {
|
||||
Options::video.supersampling.enabled = !Options::video.supersampling.enabled;
|
||||
if (Options::video.shader.enabled && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||
applyCurrentPostFXPreset();
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica los parámetros del preset actual al backend de shaders
|
||||
void Screen::applyCurrentPostFXPreset() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (shader_backend_ && !Options::postfx_presets.empty()) {
|
||||
const auto& p = Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)];
|
||||
// Supersampling es un toggle global (Options::video.supersampling.enabled), no por preset.
|
||||
// setOversample primero: puede recrear texturas antes de que setPostFXParams
|
||||
// decida si hornear scanlines en CPU o aplicarlas en GPU.
|
||||
shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
|
||||
Rendering::PostFXParams params{.vignette = p.vignette, .scanlines = p.scanlines, .chroma = p.chroma, .mask = p.mask, .gamma = p.gamma, .curvature = p.curvature, .bleeding = p.bleeding, .flicker = p.flicker};
|
||||
shader_backend_->setPostFXParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica los parámetros del preset CrtPi actual al backend de shaders
|
||||
void Screen::applyCurrentCrtPiPreset() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (shader_backend_ && !Options::crtpi_presets.empty()) {
|
||||
const auto& p = Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)];
|
||||
Rendering::CrtPiParams params{
|
||||
.scanline_weight = p.scanline_weight,
|
||||
.scanline_gap_brightness = p.scanline_gap_brightness,
|
||||
.bloom_factor = p.bloom_factor,
|
||||
.input_gamma = p.input_gamma,
|
||||
.output_gamma = p.output_gamma,
|
||||
.mask_brightness = p.mask_brightness,
|
||||
.curvature_x = p.curvature_x,
|
||||
.curvature_y = p.curvature_y,
|
||||
.mask_type = p.mask_type,
|
||||
.enable_scanlines = p.enable_scanlines,
|
||||
.enable_multisample = p.enable_multisample,
|
||||
.enable_gamma = p.enable_gamma,
|
||||
.enable_curvature = p.enable_curvature,
|
||||
.enable_sharper = p.enable_sharper,
|
||||
};
|
||||
shader_backend_->setCrtPiParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
// Cambia el shader de post-procesado activo y aplica el preset correspondiente
|
||||
void Screen::setActiveShader(Rendering::ShaderType type) {
|
||||
Options::video.shader.current_shader = type;
|
||||
if (!shader_backend_) { return; }
|
||||
if (!Options::video.shader.enabled) {
|
||||
// Shaders desactivados: guardar preferencia pero mantener pass-through
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
|
||||
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||
return;
|
||||
}
|
||||
shader_backend_->setActiveShader(type);
|
||||
if (type == Rendering::ShaderType::CRTPI) {
|
||||
applyCurrentCrtPiPreset();
|
||||
} else {
|
||||
applyCurrentPostFXPreset();
|
||||
}
|
||||
}
|
||||
|
||||
// Cicla al siguiente shader disponible (preparado para futura UI)
|
||||
void Screen::nextShader() {
|
||||
const Rendering::ShaderType NEXT = (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX)
|
||||
? Rendering::ShaderType::CRTPI
|
||||
: Rendering::ShaderType::POSTFX;
|
||||
setActiveShader(NEXT);
|
||||
}
|
||||
|
||||
// Inicializa los shaders
|
||||
// El device GPU se crea siempre (independientemente de postfx) para evitar
|
||||
// conflictos SDL_Renderer/SDL_GPU al hacer toggle F4 en Windows/Vulkan.
|
||||
void Screen::initShaders() {
|
||||
SDL_Texture* tex = Options::video.border.enabled ? border_texture_ : game_texture_;
|
||||
|
||||
if (!shader_backend_) {
|
||||
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
|
||||
const std::string FALLBACK_DRIVER = "none";
|
||||
shader_backend_->setPreferredDriver(Options::video.gpu.acceleration ? Options::video.gpu.preferred_driver : FALLBACK_DRIVER);
|
||||
}
|
||||
shader_backend_->init(window_, tex, "", "");
|
||||
gpu_driver_ = shader_backend_->getDriverName();
|
||||
|
||||
// Propagar flags de vsync, integer scale, upscale y downscale al backend GPU
|
||||
shader_backend_->setVSync(Options::video.vertical_sync);
|
||||
shader_backend_->setScaleMode(Options::video.integer_scale);
|
||||
shader_backend_->setLinearUpscale(Options::video.supersampling.linear_upscale);
|
||||
shader_backend_->setDownscaleAlgo(Options::video.supersampling.downscale_algo);
|
||||
|
||||
if (Options::video.shader.enabled) {
|
||||
applyCurrentPostFXPreset();
|
||||
} else {
|
||||
// Pass-through: todos los efectos a 0, el shader solo copia la textura
|
||||
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||
}
|
||||
|
||||
// Restaurar el shader activo guardado en config (y sus parámetros CrtPi si aplica)
|
||||
shader_backend_->setActiveShader(Options::video.shader.current_shader);
|
||||
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
|
||||
applyCurrentCrtPiPreset();
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene información sobre la pantalla
|
||||
void Screen::getDisplayInfo() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n** VIDEO SYSTEM **\n";
|
||||
|
||||
int num_displays = 0;
|
||||
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
|
||||
if (displays != nullptr) {
|
||||
for (int i = 0; i < num_displays; ++i) {
|
||||
SDL_DisplayID instance_id = displays[i];
|
||||
const char* name = SDL_GetDisplayName(instance_id);
|
||||
|
||||
std::cout << "Display " << instance_id << ": " << ((name != nullptr) ? name : "Unknown") << '\n';
|
||||
}
|
||||
|
||||
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 / Options::game.width, dm->h / Options::game.height);
|
||||
Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);
|
||||
|
||||
// Muestra información sobre el tamaño de la pantalla y de la ventana de juego
|
||||
std::cout << "Current display mode: " << static_cast<int>(dm->w) << "x" << static_cast<int>(dm->h) << " @ " << static_cast<int>(dm->refresh_rate) << "Hz\n";
|
||||
|
||||
std::cout << "Window resolution: " << static_cast<int>(Options::game.width) << "x" << static_cast<int>(Options::game.height) << " x" << Options::window.zoom << '\n';
|
||||
|
||||
Options::video.info = std::to_string(static_cast<int>(dm->w)) + "x" +
|
||||
std::to_string(static_cast<int>(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 / Options::game.width, (dm->h - WINDOWS_DECORATIONS) / Options::game.height);
|
||||
|
||||
// Normaliza los valores de zoom
|
||||
Options::window.zoom = std::min(Options::window.zoom, MAX_ZOOM);
|
||||
|
||||
SDL_free(displays);
|
||||
}
|
||||
}
|
||||
|
||||
// Arranca SDL VIDEO y crea la ventana
|
||||
auto Screen::initSDLVideo() -> bool {
|
||||
// Inicializar SDL
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
std::cerr << "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
|
||||
const auto WINDOW_WIDTH = Options::video.border.enabled ? Options::game.width + (Options::video.border.width * 2) : Options::game.width;
|
||||
const auto WINDOW_HEIGHT = Options::video.border.enabled ? Options::game.height + (Options::video.border.height * 2) : Options::game.height;
|
||||
SDL_WindowFlags window_flags = 0;
|
||||
if (Options::video.fullscreen) {
|
||||
window_flags |= SDL_WINDOW_FULLSCREEN;
|
||||
}
|
||||
window_ = SDL_CreateWindow(Options::window.caption.c_str(), WINDOW_WIDTH * Options::window.zoom, WINDOW_HEIGHT * Options::window.zoom, window_flags);
|
||||
|
||||
if (window_ == nullptr) {
|
||||
std::cerr << "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::cerr << "FATAL: Failed to create renderer! SDL Error: " << SDL_GetError() << '\n';
|
||||
SDL_DestroyWindow(window_);
|
||||
window_ = nullptr;
|
||||
SDL_Quit();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configurar renderer
|
||||
const int EXTRA_WIDTH = Options::video.border.enabled ? Options::video.border.width * 2 : 0;
|
||||
const int EXTRA_HEIGHT = Options::video.border.enabled ? Options::video.border.height * 2 : 0;
|
||||
SDL_SetRenderLogicalPresentation(
|
||||
renderer_,
|
||||
Options::game.width + EXTRA_WIDTH,
|
||||
Options::game.height + EXTRA_HEIGHT,
|
||||
Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
|
||||
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||
|
||||
std::cout << "Video system initialized successfully\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Crea el objeto de texto
|
||||
void Screen::createText() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Carga la surface de la fuente directamente del archivo
|
||||
auto surface = std::make_shared<Surface>(Resource::List::get()->get("aseprite.gif"));
|
||||
|
||||
// Crea el objeto de texto (el constructor de Text carga el archivo text_file internamente)
|
||||
text_ = std::make_shared<Text>(surface, Resource::List::get()->get("aseprite.fnt"));
|
||||
}
|
||||
184
source/core/rendering/screen.hpp
Normal file
184
source/core/rendering/screen.hpp
Normal file
@@ -0,0 +1,184 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_pixels.h> // Para Uint32
|
||||
|
||||
#include <cstddef> // Para size_t
|
||||
#include <memory> // Para shared_ptr, __shared_ptr_access
|
||||
#include <string> // Para string
|
||||
#include <utility> // Para std::pair
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/palette_manager.hpp" // Para PaletteManager
|
||||
#include "core/rendering/shader_backend.hpp" // Para Rendering::ShaderType, ShaderBackend
|
||||
#include "utils/utils.hpp" // Para Color
|
||||
class Surface;
|
||||
class Text;
|
||||
|
||||
class Screen {
|
||||
public:
|
||||
// Tipos de filtro
|
||||
enum class Filter : Uint32 {
|
||||
NEAREST = 0,
|
||||
LINEAR = 1,
|
||||
};
|
||||
|
||||
// Singleton
|
||||
static void init(); // Crea el singleton
|
||||
static void destroy(); // Destruye el singleton
|
||||
static auto get() -> Screen*; // Obtiene el singleton
|
||||
|
||||
// Renderizado
|
||||
void clearRenderer(Rgb color = {0x00, 0x00, 0x00}); // Limpia el renderer
|
||||
void clearSurface(Uint8 index); // Limpia la game_surface_
|
||||
void start(); // Prepara para empezar a dibujar en la textura de juego
|
||||
void render(); // Vuelca el contenido del renderizador en pantalla
|
||||
void update(float delta_time); // Actualiza la lógica de la clase
|
||||
|
||||
// Video y ventana
|
||||
void setVideoMode(bool mode); // Establece el modo de video
|
||||
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
|
||||
void toggleIntegerScale(); // Alterna entre activar y desactivar el escalado entero
|
||||
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
|
||||
auto decWindowZoom() -> bool; // Reduce el tamaño de la ventana
|
||||
auto incWindowZoom() -> bool; // Aumenta el tamaño de la ventana
|
||||
auto setWindowZoom(int zoom) -> bool; // Establece zoom directo; false si fuera de [1, max_zoom]
|
||||
void show(); // Muestra la ventana
|
||||
void hide(); // Oculta la ventana
|
||||
|
||||
// Borde
|
||||
void setBorderColor(Uint8 color); // Cambia el color del borde
|
||||
static void setBorderWidth(int width); // Establece el ancho del borde
|
||||
static void setBorderHeight(int height); // Establece el alto del borde
|
||||
static void setBorderEnabled(bool value); // Establece si se ha de ver el borde
|
||||
void toggleBorder(); // Cambia entre borde visible y no visible
|
||||
|
||||
// Paletas y PostFX
|
||||
void nextPalette(); // Cambia a la siguiente paleta
|
||||
void previousPalette(); // Cambia a la paleta anterior
|
||||
auto setPaletteByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe
|
||||
[[nodiscard]] auto getPaletteNames() const -> std::vector<std::string>; // Nombres disponibles (minúsculas, sin .pal)
|
||||
[[nodiscard]] auto getPalettePrettyName() const -> std::string; // Nombre actual con guiones sustituidos por espacios
|
||||
void nextPaletteSortMode(); // Cicla al siguiente modo de ordenación de paleta
|
||||
void setPaletteSortMode(PaletteSortMode mode); // Establece modo de ordenación concreto
|
||||
[[nodiscard]] auto getPaletteSortModeName() const -> std::string; // Nombre del modo de ordenación actual
|
||||
void toggleShaders(); // Activa/desactiva todos los shaders respetando current_shader
|
||||
void toggleSupersampling(); // Activa/desactiva el supersampling global
|
||||
void reloadPostFX(); // Recarga el shader del preset actual sin toggle
|
||||
void reloadCrtPi(); // Recarga el shader CrtPi del preset actual sin toggle
|
||||
void setLinearUpscale(bool linear); // Upscale NEAREST (false) o LINEAR (true) en el paso SS
|
||||
void setDownscaleAlgo(int algo); // 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
|
||||
void setActiveShader(Rendering::ShaderType type); // Cambia el shader de post-procesado activo
|
||||
void nextShader(); // Cicla al siguiente shader disponible (para futura UI)
|
||||
|
||||
// Surfaces y notificaciones
|
||||
void setRendererSurface(const std::shared_ptr<Surface>& surface = nullptr); // Establece el renderizador para las surfaces
|
||||
void setNotificationsEnabled(bool value); // Establece la visibilidad de las notificaciones
|
||||
void updateZoomFactor(); // Recalcula y almacena el factor de zoom real
|
||||
|
||||
// Getters
|
||||
auto getRenderer() -> SDL_Renderer*;
|
||||
auto getRendererSurface() -> std::shared_ptr<Surface>;
|
||||
auto getBorderSurface() -> std::shared_ptr<Surface>;
|
||||
auto getGameSurface() -> std::shared_ptr<Surface>;
|
||||
[[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; }
|
||||
[[nodiscard]] auto getGameSurfaceDstRect() const -> SDL_FRect { return game_surface_dstrect_; }
|
||||
[[nodiscard]] auto getGPUDriver() const -> const std::string& { return gpu_driver_; }
|
||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool { return shader_backend_ && shader_backend_->isHardwareAccelerated(); }
|
||||
[[nodiscard]] auto getLastFPS() const -> int { return fps_.last_value; }
|
||||
[[nodiscard]] auto getZoomFactor() const -> float { return zoom_factor_; }
|
||||
[[nodiscard]] static auto getMaxZoom() -> int;
|
||||
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int>;
|
||||
|
||||
private:
|
||||
// Estructuras
|
||||
struct DisplayMonitor {
|
||||
std::string name;
|
||||
int width{0};
|
||||
int height{0};
|
||||
int refresh_rate{0};
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// Constantes
|
||||
static constexpr int WINDOWS_DECORATIONS = 35; // Decoraciones de la ventana
|
||||
|
||||
// Singleton
|
||||
static Screen* screen;
|
||||
|
||||
// Métodos privados
|
||||
void renderNotifications() const; // Dibuja las notificaciones
|
||||
void adjustWindowSize(); // Calcula el tamaño de la ventana
|
||||
void adjustRenderLogicalSize(); // Ajusta el tamaño lógico del renderizador
|
||||
void surfaceToTexture(); // Copia la surface a la textura
|
||||
void textureToRenderer(); // Copia la textura al renderizador
|
||||
void renderOverlays(); // Renderiza todos los overlays
|
||||
void initShaders(); // Inicializa los shaders
|
||||
void applyCurrentPostFXPreset(); // Aplica los parámetros del preset PostFX actual al backend
|
||||
void applyCurrentCrtPiPreset(); // Aplica los parámetros del preset CrtPi actual al backend
|
||||
void getDisplayInfo(); // Obtiene información sobre la pantalla
|
||||
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
|
||||
void createText(); // Crea el objeto de texto
|
||||
|
||||
// Constructor y destructor
|
||||
Screen();
|
||||
~Screen();
|
||||
|
||||
// Objetos SDL
|
||||
SDL_Window* window_{nullptr}; // Ventana de la aplicación
|
||||
SDL_Renderer* renderer_{nullptr}; // Renderizador de la ventana
|
||||
SDL_Texture* game_texture_{nullptr}; // Textura donde se dibuja el juego
|
||||
SDL_Texture* border_texture_{nullptr}; // Textura donde se dibuja el borde del juego
|
||||
|
||||
// Surfaces y renderizado
|
||||
std::shared_ptr<Surface> game_surface_; // Surface principal del juego
|
||||
std::shared_ptr<Surface> border_surface_; // Surface para el borde de la pantalla
|
||||
std::shared_ptr<std::shared_ptr<Surface>> renderer_surface_; // Puntero a la Surface activa
|
||||
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend de shaders (OpenGL/Metal/Vulkan)
|
||||
std::shared_ptr<Text> text_; // Objeto para escribir texto
|
||||
|
||||
// Buffers persistentes para evitar .resize() cada frame
|
||||
std::vector<Uint32> game_pixel_buffer_; // Textura de juego
|
||||
std::vector<Uint32> border_pixel_buffer_; // Textura de borde (composición final borde+juego)
|
||||
|
||||
// Caché del borde sólido (gameplay normal)
|
||||
bool border_is_solid_{true}; // true = borde de color sólido; false = borde dinámico (carga)
|
||||
Uint32 border_argb_color_{0}; // Color ARGB pre-convertido del borde sólido
|
||||
|
||||
// Configuración de ventana y pantalla
|
||||
int window_width_{0}; // Ancho de la pantalla o ventana
|
||||
int window_height_{0}; // Alto de la pantalla o ventana
|
||||
float zoom_factor_{1.0F}; // Factor de zoom calculado (alto físico / alto lógico)
|
||||
SDL_FRect game_surface_dstrect_; // Coordenadas donde se dibuja la textura del juego
|
||||
|
||||
// Paletas y colores
|
||||
Uint8 border_color_{0}; // Color del borde
|
||||
std::unique_ptr<PaletteManager> palette_manager_; // Gestor de paletas de color
|
||||
|
||||
// Estado y configuración
|
||||
bool notifications_enabled_{false}; // Indica si se muestran las notificaciones
|
||||
FPS fps_; // Gestor de frames por segundo
|
||||
DisplayMonitor display_monitor_; // Información de la pantalla
|
||||
|
||||
// Shaders
|
||||
std::string info_resolution_; // Texto con la información de la pantalla
|
||||
std::string gpu_driver_; // Nombre del driver GPU (SDL3GPU), capturado en initShaders()
|
||||
};
|
||||
10362
source/core/rendering/sdl3gpu/crtpi_frag_spv.h
Normal file
10362
source/core/rendering/sdl3gpu/crtpi_frag_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
4253
source/core/rendering/sdl3gpu/downscale_frag_spv.h
Normal file
4253
source/core/rendering/sdl3gpu/downscale_frag_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
11717
source/core/rendering/sdl3gpu/postfx_frag_spv.h
Normal file
11717
source/core/rendering/sdl3gpu/postfx_frag_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
1449
source/core/rendering/sdl3gpu/postfx_vert_spv.h
Normal file
1449
source/core/rendering/sdl3gpu/postfx_vert_spv.h
Normal file
File diff suppressed because it is too large
Load Diff
1329
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
Normal file
1329
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
Normal file
File diff suppressed because it is too large
Load Diff
178
source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp
Normal file
178
source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp
Normal file
@@ -0,0 +1,178 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include "core/rendering/shader_backend.hpp"
|
||||
|
||||
// PostFX uniforms pushed to fragment stage each frame.
|
||||
// Must match the MSL struct and GLSL uniform block layout.
|
||||
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.
|
||||
struct PostFXUniforms {
|
||||
float vignette_strength; // 0 = none, ~0.8 = subtle
|
||||
float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration
|
||||
float scanline_strength; // 0 = off, 1 = full
|
||||
float screen_height; // logical height in pixels (used by bleeding effect)
|
||||
float mask_strength; // 0 = off, 1 = full phosphor dot mask
|
||||
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
|
||||
float curvature; // 0 = flat, 1 = max barrel distortion
|
||||
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
|
||||
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
|
||||
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — keep struct at 48 bytes (3 × 16)
|
||||
};
|
||||
|
||||
// CrtPi uniforms pushed to fragment stage each frame.
|
||||
// Must match the MSL struct and GLSL uniform block layout.
|
||||
// 14 fields (8 floats + 6 ints) + 2 floats (texture size) = 16 fields = 64 bytes — 4 × 16-byte alignment.
|
||||
struct CrtPiUniforms {
|
||||
// vec4 #0
|
||||
float scanline_weight; // Ajuste gaussiano (default 6.0)
|
||||
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
|
||||
float bloom_factor; // Factor brillo zonas iluminadas (default 3.5)
|
||||
float input_gamma; // Gamma de entrada (default 2.4)
|
||||
// vec4 #1
|
||||
float output_gamma; // Gamma de salida (default 2.2)
|
||||
float mask_brightness; // Brillo sub-píxeles máscara (default 0.80)
|
||||
float curvature_x; // Distorsión barrel X (default 0.05)
|
||||
float curvature_y; // Distorsión barrel Y (default 0.10)
|
||||
// vec4 #2
|
||||
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
|
||||
int enable_scanlines; // 0 = off, 1 = on
|
||||
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico)
|
||||
int enable_gamma; // 0 = off, 1 = on
|
||||
// vec4 #3
|
||||
int enable_curvature; // 0 = off, 1 = on
|
||||
int enable_sharper; // 0 = off, 1 = on
|
||||
float texture_width; // Ancho del canvas en píxeles (inyectado en render)
|
||||
float texture_height; // Alto del canvas en píxeles (inyectado en render)
|
||||
};
|
||||
|
||||
// Downscale uniforms pushed to the Lanczos downscale fragment stage.
|
||||
// 1 int + 3 floats = 16 bytes — meets Metal/Vulkan alignment.
|
||||
struct DownscaleUniforms {
|
||||
int algorithm; // 0 = Lanczos2 (ventana 2), 1 = Lanczos3 (ventana 3)
|
||||
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)
|
||||
*
|
||||
* Reemplaza el backend OpenGL para que los shaders PostFX funcionen en macOS.
|
||||
* Pipeline: Surface pixels (CPU) → SDL_GPUTransferBuffer → SDL_GPUTexture (scene)
|
||||
* → PostFX render pass → 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; // Libera pipeline/texturas pero mantiene el device vivo
|
||||
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
|
||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
|
||||
[[nodiscard]] auto getDriverName() const -> std::string override { return driver_name_; }
|
||||
|
||||
// Establece el driver GPU preferido (vacío = auto). Debe llamarse antes de init().
|
||||
void setPreferredDriver(const std::string& driver) override { preferred_driver_ = driver; }
|
||||
|
||||
// Sube píxeles ARGB8888 desde CPU; llamado antes de render()
|
||||
void uploadPixels(const Uint32* pixels, int width, int height) override;
|
||||
|
||||
// Actualiza los parámetros de intensidad de los efectos PostFX
|
||||
void setPostFXParams(const PostFXParams& p) override;
|
||||
|
||||
// Activa/desactiva VSync en el swapchain
|
||||
void setVSync(bool vsync) override;
|
||||
|
||||
// Activa/desactiva escalado entero (integer scale)
|
||||
void setScaleMode(bool integer_scale) override;
|
||||
|
||||
// Establece factor de supersampling (1 = off, 3 = 3×SS)
|
||||
void setOversample(int factor) override;
|
||||
|
||||
// Activa/desactiva interpolación LINEAR en el upscale (false = NEAREST)
|
||||
void setLinearUpscale(bool linear) override;
|
||||
|
||||
// Selecciona algoritmo de downscale: 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
|
||||
void setDownscaleAlgo(int algo) override;
|
||||
|
||||
// Devuelve las dimensiones de la textura de supersampling (0,0 si SS desactivado)
|
||||
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int> override;
|
||||
|
||||
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
|
||||
void setActiveShader(ShaderType type) override;
|
||||
|
||||
// Actualiza los parámetros del shader CRT-Pi
|
||||
void setCrtPiParams(const CrtPiParams& p) override;
|
||||
|
||||
// Devuelve el shader activo
|
||||
[[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; // Pipeline dedicado para el shader CrtPi
|
||||
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
||||
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado
|
||||
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
|
||||
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
|
||||
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
||||
|
||||
SDL_Window* window_ = nullptr;
|
||||
SDL_GPUDevice* device_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass (→ swapchain o → postfx_texture_)
|
||||
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass (→ swapchain directo, sin SS)
|
||||
SDL_GPUGraphicsPipeline* postfx_offscreen_pipeline_ = nullptr; // PostFX → postfx_texture_ (B8G8R8A8, solo con Lanczos)
|
||||
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
|
||||
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
|
||||
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del juego (game_width_ × game_height_)
|
||||
SDL_GPUTexture* scaled_texture_ = nullptr; // Upscale target (game×factor), solo con SS
|
||||
SDL_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolución escalada, solo con Lanczos
|
||||
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
|
||||
SDL_GPUSampler* sampler_ = nullptr; // NEAREST
|
||||
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR
|
||||
|
||||
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; // Shader de post-procesado activo
|
||||
|
||||
int game_width_ = 0; // Dimensiones originales del canvas
|
||||
int game_height_ = 0;
|
||||
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado
|
||||
int oversample_ = 1; // SS on/off (1 = off, >1 = on)
|
||||
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
|
||||
std::string driver_name_;
|
||||
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
||||
bool is_initialized_ = false;
|
||||
bool vsync_ = true;
|
||||
bool integer_scale_ = false;
|
||||
bool linear_upscale_ = false; // Upscale NEAREST (false) o LINEAR (true)
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
633
source/core/rendering/sdl3gpu/upscale_frag_spv.h
Normal file
633
source/core/rendering/sdl3gpu/upscale_frag_spv.h
Normal file
@@ -0,0 +1,633 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
static const uint8_t kupscale_frag_spv[] = {
|
||||
0x03,
|
||||
0x02,
|
||||
0x23,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x14,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x53,
|
||||
0x4c,
|
||||
0x2e,
|
||||
0x73,
|
||||
0x74,
|
||||
0x64,
|
||||
0x2e,
|
||||
0x34,
|
||||
0x35,
|
||||
0x30,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x6d,
|
||||
0x61,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xc2,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x5f,
|
||||
0x47,
|
||||
0x4f,
|
||||
0x4f,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x45,
|
||||
0x5f,
|
||||
0x63,
|
||||
0x70,
|
||||
0x70,
|
||||
0x5f,
|
||||
0x73,
|
||||
0x74,
|
||||
0x79,
|
||||
0x6c,
|
||||
0x65,
|
||||
0x5f,
|
||||
0x6c,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x65,
|
||||
0x5f,
|
||||
0x64,
|
||||
0x69,
|
||||
0x72,
|
||||
0x65,
|
||||
0x63,
|
||||
0x74,
|
||||
0x69,
|
||||
0x76,
|
||||
0x65,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x5f,
|
||||
0x47,
|
||||
0x4f,
|
||||
0x4f,
|
||||
0x47,
|
||||
0x4c,
|
||||
0x45,
|
||||
0x5f,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x63,
|
||||
0x6c,
|
||||
0x75,
|
||||
0x64,
|
||||
0x65,
|
||||
0x5f,
|
||||
0x64,
|
||||
0x69,
|
||||
0x72,
|
||||
0x65,
|
||||
0x63,
|
||||
0x74,
|
||||
0x69,
|
||||
0x76,
|
||||
0x65,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x6d,
|
||||
0x61,
|
||||
0x69,
|
||||
0x6e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x6f,
|
||||
0x75,
|
||||
0x74,
|
||||
0x5f,
|
||||
0x63,
|
||||
0x6f,
|
||||
0x6c,
|
||||
0x6f,
|
||||
0x72,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x73,
|
||||
0x63,
|
||||
0x65,
|
||||
0x6e,
|
||||
0x65,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x76,
|
||||
0x5f,
|
||||
0x75,
|
||||
0x76,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x22,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x47,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x16,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x17,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x08,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x19,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1b,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0c,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0c,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x17,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3b,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x36,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xf8,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3d,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0b,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3d,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0f,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x12,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x11,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x57,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0e,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x12,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3e,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x13,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xfd,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x38,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00};
|
||||
static const size_t kupscale_frag_spv_size = 628;
|
||||
175
source/core/rendering/shader_backend.hpp
Normal file
175
source/core/rendering/shader_backend.hpp
Normal file
@@ -0,0 +1,175 @@
|
||||
#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
|
||||
* Definido a nivel de namespace para facilitar el uso desde subclases y screen.cpp
|
||||
*/
|
||||
struct PostFXParams {
|
||||
float vignette = 0.0F; // Intensidad de la viñeta
|
||||
float scanlines = 0.0F; // Intensidad de las scanlines
|
||||
float chroma = 0.0F; // Aberración cromática
|
||||
float mask = 0.0F; // Máscara de fósforo RGB
|
||||
float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full)
|
||||
float curvature = 0.0F; // Curvatura barrel CRT
|
||||
float bleeding = 0.0F; // Sangrado de color NTSC
|
||||
float flicker = 0.0F; // Parpadeo de fósforo CRT ~50 Hz
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Parámetros del shader CRT-Pi (algoritmo de scanlines continuas)
|
||||
* Diferente al PostFX: usa pesos gaussianos por distancia subpixel y bloom.
|
||||
*/
|
||||
struct CrtPiParams {
|
||||
float scanline_weight{6.0F}; // Ajuste gaussiano (mayor = scanlines más estrechas)
|
||||
float scanline_gap_brightness{0.12F}; // Brillo mínimo en las ranuras entre scanlines
|
||||
float bloom_factor{3.5F}; // Factor de brillo para zonas iluminadas
|
||||
float input_gamma{2.4F}; // Gamma de entrada (linealización)
|
||||
float output_gamma{2.2F}; // Gamma de salida (codificación)
|
||||
float mask_brightness{0.80F}; // Sub-píxeles tenues en la máscara de fósforo
|
||||
float curvature_x{0.05F}; // Distorsión barrel eje X
|
||||
float curvature_y{0.10F}; // Distorsión barrel eje Y
|
||||
int mask_type{2}; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
|
||||
bool enable_scanlines{true}; // Activar efecto de scanlines
|
||||
bool enable_multisample{true}; // Antialiasing analítico de scanlines
|
||||
bool enable_gamma{true}; // Corrección gamma
|
||||
bool enable_curvature{false}; // Distorsión barrel CRT
|
||||
bool enable_sharper{false}; // Submuestreo más nítido (modo SHARPER)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Interfaz abstracta para backends de renderizado con shaders
|
||||
*
|
||||
* Esta interfaz define el contrato que todos los backends de shaders
|
||||
* deben cumplir (OpenGL, Metal, Vulkan, etc.)
|
||||
*/
|
||||
class ShaderBackend {
|
||||
public:
|
||||
virtual ~ShaderBackend() = default;
|
||||
|
||||
/**
|
||||
* @brief Inicializa el backend de shaders
|
||||
* @param window Ventana SDL
|
||||
* @param texture Textura de backbuffer a la que aplicar shaders
|
||||
* @param vertex_source Código fuente del vertex shader
|
||||
* @param fragment_source Código fuente del fragment shader
|
||||
* @return true si la inicialización fue exitosa
|
||||
*/
|
||||
virtual auto init(SDL_Window* window,
|
||||
SDL_Texture* texture,
|
||||
const std::string& vertex_source,
|
||||
const std::string& fragment_source) -> bool = 0;
|
||||
|
||||
/**
|
||||
* @brief Renderiza la textura con los shaders aplicados
|
||||
*/
|
||||
virtual void render() = 0;
|
||||
|
||||
/**
|
||||
* @brief Establece el tamaño de la textura como parámetro del shader
|
||||
* @param width Ancho de la textura
|
||||
* @param height Alto de la textura
|
||||
*/
|
||||
virtual void setTextureSize(float width, float height) = 0;
|
||||
|
||||
/**
|
||||
* @brief Limpia y libera recursos del backend
|
||||
*/
|
||||
virtual void cleanup() = 0;
|
||||
|
||||
/**
|
||||
* @brief Sube píxeles ARGB8888 desde la CPU al backend de shaders
|
||||
* Usado por SDL3GPUShader para evitar pasar por SDL_Texture
|
||||
*/
|
||||
virtual void uploadPixels(const Uint32* /*pixels*/, int /*width*/, int /*height*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece los parámetros de intensidad de los efectos PostFX
|
||||
* @param p Struct con todos los parámetros PostFX
|
||||
*/
|
||||
virtual void setPostFXParams(const PostFXParams& /*p*/) {}
|
||||
|
||||
/**
|
||||
* @brief Activa o desactiva VSync en el swapchain del GPU device
|
||||
*/
|
||||
virtual void setVSync(bool /*vsync*/) {}
|
||||
|
||||
/**
|
||||
* @brief Activa o desactiva el escalado entero (integer scale)
|
||||
*/
|
||||
virtual void setScaleMode(bool /*integer_scale*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
|
||||
* Con factor > 1, la textura GPU se crea a game×factor resolución y
|
||||
* las scanlines se hornean en CPU (uploadPixels). El sampler usa LINEAR.
|
||||
*/
|
||||
virtual void setOversample(int /*factor*/) {}
|
||||
|
||||
/**
|
||||
* @brief Activa/desactiva interpolación LINEAR en el paso de upscale (SS).
|
||||
* Por defecto NEAREST (false). Solo tiene efecto con supersampling activo.
|
||||
*/
|
||||
virtual void setLinearUpscale(bool /*linear*/) {}
|
||||
[[nodiscard]] virtual auto isLinearUpscale() const -> bool { return false; }
|
||||
|
||||
/**
|
||||
* @brief Selecciona el algoritmo de downscale tras el PostFX (SS activo).
|
||||
* 0 = bilinear legacy (comportamiento actual, sin textura intermedia),
|
||||
* 1 = Lanczos2 (ventana 2, ~25 muestras), 2 = Lanczos3 (ventana 3, ~49 muestras).
|
||||
*/
|
||||
virtual void setDownscaleAlgo(int /*algo*/) {}
|
||||
[[nodiscard]] virtual auto getDownscaleAlgo() const -> int { return 0; }
|
||||
|
||||
/**
|
||||
* @brief Devuelve las dimensiones de la textura de supersampling.
|
||||
* @return Par (ancho, alto) en píxeles; (0, 0) si SS está desactivado.
|
||||
*/
|
||||
[[nodiscard]] virtual auto getSsTextureSize() const -> std::pair<int, int> { return {0, 0}; }
|
||||
|
||||
/**
|
||||
* @brief Verifica si el backend está usando aceleración por hardware
|
||||
* @return true si usa aceleración (OpenGL/Metal/Vulkan)
|
||||
*/
|
||||
[[nodiscard]] virtual auto isHardwareAccelerated() const -> bool = 0;
|
||||
|
||||
/**
|
||||
* @brief Nombre del driver GPU activo (p.ej. "vulkan", "metal", "direct3d12")
|
||||
* @return Cadena vacía si no disponible
|
||||
*/
|
||||
[[nodiscard]] virtual auto getDriverName() const -> std::string { return {}; }
|
||||
|
||||
/**
|
||||
* @brief Establece el driver GPU preferido antes de init().
|
||||
* Vacío = selección automática de SDL. Implementado en SDL3GPUShader.
|
||||
*/
|
||||
virtual void setPreferredDriver(const std::string& /*driver*/) {}
|
||||
|
||||
/**
|
||||
* @brief Selecciona el shader de post-procesado activo (POSTFX o CRTPI).
|
||||
* Debe llamarse antes de render(). No recrea pipelines.
|
||||
*/
|
||||
virtual void setActiveShader(ShaderType /*type*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece los parámetros del shader CRT-Pi.
|
||||
*/
|
||||
virtual void setCrtPiParams(const CrtPiParams& /*p*/) {}
|
||||
|
||||
/**
|
||||
* @brief Devuelve el shader de post-procesado activo.
|
||||
*/
|
||||
[[nodiscard]] virtual auto getActiveShader() const -> ShaderType { return ShaderType::POSTFX; }
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
344
source/core/rendering/sprite/animated_sprite.cpp
Normal file
344
source/core/rendering/sprite/animated_sprite.cpp
Normal file
@@ -0,0 +1,344 @@
|
||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||
|
||||
#include <cstddef> // Para size_t
|
||||
#include <fstream> // Para basic_ostream, basic_istream, operator<<, basic...
|
||||
#include <iostream> // Para cout, cerr
|
||||
#include <sstream> // Para basic_stringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||
#include "utils/utils.hpp" // Para printWithDots
|
||||
|
||||
// Helper: Convierte un nodo YAML de frames (array) a vector de SDL_FRect
|
||||
auto convertYAMLFramesToRects(const fkyaml::node& frames_node, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector<SDL_FRect> {
|
||||
std::vector<SDL_FRect> frames;
|
||||
SDL_FRect rect = {.x = 0.0F, .y = 0.0F, .w = frame_width, .h = frame_height};
|
||||
|
||||
for (const auto& frame_index_node : frames_node) {
|
||||
const int NUM_TILE = frame_index_node.get_value<int>();
|
||||
if (NUM_TILE <= max_tiles) {
|
||||
rect.x = (NUM_TILE % frames_per_row) * frame_width;
|
||||
rect.y = (NUM_TILE / frames_per_row) * frame_height;
|
||||
frames.emplace_back(rect);
|
||||
}
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
// Carga las animaciones desde un fichero YAML
|
||||
auto AnimatedSprite::loadAnimationsFromYAML(const std::string& file_path, std::shared_ptr<Surface>& surface, float& frame_width, float& frame_height) -> std::vector<AnimationData> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::vector<AnimationData> animations;
|
||||
|
||||
// Extract filename for logging
|
||||
const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1);
|
||||
|
||||
try {
|
||||
// Load YAML file using ResourceHelper (supports both filesystem and pack)
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
|
||||
if (file_data.empty()) {
|
||||
std::cerr << "Error: Unable to load animation file " << FILE_NAME << '\n';
|
||||
throw std::runtime_error("Animation file not found: " + file_path);
|
||||
}
|
||||
|
||||
printWithDots("Animation : ", FILE_NAME, "[ LOADED ]");
|
||||
|
||||
// Parse YAML from string
|
||||
std::string yaml_content(file_data.begin(), file_data.end());
|
||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||
|
||||
// --- Parse global configuration ---
|
||||
if (yaml.contains("tileSetFile")) {
|
||||
auto tile_set_file = yaml["tileSetFile"].get_value<std::string>();
|
||||
surface = Resource::Cache::get()->getSurface(tile_set_file);
|
||||
}
|
||||
|
||||
if (yaml.contains("frameWidth")) {
|
||||
frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
|
||||
}
|
||||
|
||||
if (yaml.contains("frameHeight")) {
|
||||
frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
|
||||
}
|
||||
|
||||
// Calculate sprite sheet parameters
|
||||
int frames_per_row = 1;
|
||||
int max_tiles = 1;
|
||||
if (surface) {
|
||||
frames_per_row = surface->getWidth() / static_cast<int>(frame_width);
|
||||
const int W = surface->getWidth() / static_cast<int>(frame_width);
|
||||
const int H = surface->getHeight() / static_cast<int>(frame_height);
|
||||
max_tiles = W * H;
|
||||
}
|
||||
|
||||
// --- Parse animations array ---
|
||||
if (yaml.contains("animations") && yaml["animations"].is_sequence()) {
|
||||
const auto& animations_node = yaml["animations"];
|
||||
|
||||
for (const auto& anim_node : animations_node) {
|
||||
AnimationData animation;
|
||||
|
||||
// Parse animation name
|
||||
if (anim_node.contains("name")) {
|
||||
animation.name = anim_node["name"].get_value<std::string>();
|
||||
}
|
||||
|
||||
// Parse speed (seconds per frame)
|
||||
if (anim_node.contains("speed")) {
|
||||
animation.speed = anim_node["speed"].get_value<float>();
|
||||
}
|
||||
|
||||
// Parse loop frame index
|
||||
if (anim_node.contains("loop")) {
|
||||
animation.loop = anim_node["loop"].get_value<int>();
|
||||
}
|
||||
|
||||
// Parse frames array
|
||||
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||
animation.frames = convertYAMLFramesToRects(
|
||||
anim_node["frames"],
|
||||
frame_width,
|
||||
frame_height,
|
||||
frames_per_row,
|
||||
max_tiles);
|
||||
}
|
||||
|
||||
animations.push_back(animation);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "YAML parsing error in " << FILE_NAME << ": " << e.what() << '\n';
|
||||
throw;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading animation " << FILE_NAME << ": " << e.what() << '\n';
|
||||
throw;
|
||||
}
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
// Constructor con bytes YAML del cache (parsing lazy)
|
||||
AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
|
||||
// Parsear YAML desde los bytes cargados en cache
|
||||
std::string yaml_content(cached_data.yaml_data.begin(), cached_data.yaml_data.end());
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||
|
||||
// Variables para almacenar configuración global
|
||||
float frame_width = 0.0F;
|
||||
float frame_height = 0.0F;
|
||||
|
||||
// --- Parse global configuration ---
|
||||
if (yaml.contains("tileSetFile")) {
|
||||
auto tile_set_file = yaml["tileSetFile"].get_value<std::string>();
|
||||
// Ahora SÍ podemos acceder al cache (ya está completamente cargado)
|
||||
surface_ = Resource::Cache::get()->getSurface(tile_set_file);
|
||||
}
|
||||
|
||||
if (yaml.contains("frameWidth")) {
|
||||
frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
|
||||
}
|
||||
|
||||
if (yaml.contains("frameHeight")) {
|
||||
frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
|
||||
}
|
||||
|
||||
// Calculate sprite sheet parameters
|
||||
int frames_per_row = 1;
|
||||
int max_tiles = 1;
|
||||
if (surface_) {
|
||||
frames_per_row = surface_->getWidth() / static_cast<int>(frame_width);
|
||||
const int W = surface_->getWidth() / static_cast<int>(frame_width);
|
||||
const int H = surface_->getHeight() / static_cast<int>(frame_height);
|
||||
max_tiles = W * H;
|
||||
}
|
||||
|
||||
// --- Parse animations array ---
|
||||
if (yaml.contains("animations") && yaml["animations"].is_sequence()) {
|
||||
const auto& animations_node = yaml["animations"];
|
||||
|
||||
for (const auto& anim_node : animations_node) {
|
||||
AnimationData animation;
|
||||
|
||||
// Parse animation name
|
||||
if (anim_node.contains("name")) {
|
||||
animation.name = anim_node["name"].get_value<std::string>();
|
||||
}
|
||||
|
||||
// Parse speed (seconds per frame)
|
||||
if (anim_node.contains("speed")) {
|
||||
animation.speed = anim_node["speed"].get_value<float>();
|
||||
}
|
||||
|
||||
// Parse loop frame index
|
||||
if (anim_node.contains("loop")) {
|
||||
animation.loop = anim_node["loop"].get_value<int>();
|
||||
}
|
||||
|
||||
// Parse frames array
|
||||
if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
|
||||
animation.frames = convertYAMLFramesToRects(
|
||||
anim_node["frames"],
|
||||
frame_width,
|
||||
frame_height,
|
||||
frames_per_row,
|
||||
max_tiles);
|
||||
}
|
||||
|
||||
animations_.push_back(animation);
|
||||
}
|
||||
}
|
||||
|
||||
// Set dimensions
|
||||
setWidth(frame_width);
|
||||
setHeight(frame_height);
|
||||
|
||||
// Inicializar con la primera animación si existe
|
||||
if (!animations_.empty() && !animations_[0].frames.empty()) {
|
||||
setClip(animations_[0].frames[0]);
|
||||
}
|
||||
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
|
||||
throw;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading animation " << cached_data.name << ": " << e.what() << '\n';
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Constructor per a subclasses amb surface directa (sense YAML)
|
||||
AnimatedSprite::AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||
: MovingSprite(std::move(surface), pos) {
|
||||
// animations_ queda buit (protegit per el guard de animate())
|
||||
if (surface_) {
|
||||
clip_ = {.x = 0, .y = 0, .w = surface_->getWidth(), .h = surface_->getHeight()};
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene el indice de la animación a partir del nombre
|
||||
auto AnimatedSprite::getIndex(const std::string& name) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto index = -1;
|
||||
|
||||
for (const auto& a : animations_) {
|
||||
index++;
|
||||
if (a.name == name) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
std::cout << "** Warning: could not find \"" << name.c_str() << "\" animation" << '\n';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calcula el frame correspondiente a la animación (time-based)
|
||||
void AnimatedSprite::animate(float delta_time) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (animations_.empty()) { return; }
|
||||
if (animations_[current_animation_].speed <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Acumula el tiempo transcurrido
|
||||
animations_[current_animation_].accumulated_time += delta_time;
|
||||
|
||||
// Calcula el frame actual a partir del tiempo acumulado
|
||||
const int TARGET_FRAME = static_cast<int>(
|
||||
animations_[current_animation_].accumulated_time /
|
||||
animations_[current_animation_].speed);
|
||||
|
||||
// Si alcanza el final de la animación, maneja el loop
|
||||
if (TARGET_FRAME >= static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||
if (animations_[current_animation_].loop == -1) {
|
||||
// Si no hay loop, congela en el último frame
|
||||
animations_[current_animation_].current_frame =
|
||||
static_cast<int>(animations_[current_animation_].frames.size()) - 1;
|
||||
animations_[current_animation_].completed = true;
|
||||
|
||||
// Establece el clip del último frame
|
||||
if (animations_[current_animation_].current_frame >= 0) {
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
} else {
|
||||
// Si hay loop, vuelve al frame indicado
|
||||
animations_[current_animation_].accumulated_time =
|
||||
static_cast<float>(animations_[current_animation_].loop) *
|
||||
animations_[current_animation_].speed;
|
||||
animations_[current_animation_].current_frame = animations_[current_animation_].loop;
|
||||
|
||||
// Establece el clip del frame de loop
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
} else {
|
||||
// Actualiza el frame actual
|
||||
animations_[current_animation_].current_frame = TARGET_FRAME;
|
||||
|
||||
// Establece el clip del frame actual
|
||||
if (animations_[current_animation_].current_frame >= 0 &&
|
||||
animations_[current_animation_].current_frame <
|
||||
static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const auto NEW_ANIMATION = getIndex(name);
|
||||
if (current_animation_ != NEW_ANIMATION) {
|
||||
current_animation_ = NEW_ANIMATION;
|
||||
animations_[current_animation_].current_frame = 0;
|
||||
animations_[current_animation_].accumulated_time = 0.0F;
|
||||
animations_[current_animation_].completed = false;
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
}
|
||||
|
||||
// Establece la animacion actual
|
||||
void AnimatedSprite::setCurrentAnimation(int index) {
|
||||
const auto NEW_ANIMATION = index;
|
||||
if (current_animation_ != NEW_ANIMATION) {
|
||||
current_animation_ = NEW_ANIMATION;
|
||||
animations_[current_animation_].current_frame = 0;
|
||||
animations_[current_animation_].accumulated_time = 0.0F;
|
||||
animations_[current_animation_].completed = false;
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
}
|
||||
|
||||
// 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_].accumulated_time = 0.0F;
|
||||
animations_[current_animation_].completed = false;
|
||||
}
|
||||
|
||||
// Establece el frame actual de la animación
|
||||
void AnimatedSprite::setCurrentAnimationFrame(int num) {
|
||||
// Descarta valores fuera de rango
|
||||
if (num < 0 || num >= static_cast<int>(animations_[current_animation_].frames.size())) {
|
||||
num = 0;
|
||||
}
|
||||
|
||||
// Cambia el valor de la variable
|
||||
animations_[current_animation_].current_frame = num;
|
||||
|
||||
// Escoge el frame correspondiente de la animación
|
||||
setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
|
||||
}
|
||||
60
source/core/rendering/sprite/animated_sprite.hpp
Normal file
60
source/core/rendering/sprite/animated_sprite.hpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
#include <utility>
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/sprite/moving_sprite.hpp" // Para SMovingSprite
|
||||
#include "core/resources/resource_types.hpp" // Para AnimationResource
|
||||
|
||||
class Surface;
|
||||
|
||||
class AnimatedSprite : public MovingSprite {
|
||||
public:
|
||||
using Animations = std::vector<std::string>; // Tipo para lista de animaciones
|
||||
|
||||
// Estructura pública de datos de animación
|
||||
struct AnimationData {
|
||||
std::string name; // Nombre de la animacion
|
||||
std::vector<SDL_FRect> frames; // Cada uno de los frames que componen la animación
|
||||
float speed{0.083F}; // Velocidad de la animación (segundos por frame)
|
||||
int loop{0}; // Indica a que frame vuelve la animación al terminar. -1 para que no vuelva
|
||||
bool completed{false}; // Indica si ha finalizado la animación
|
||||
int current_frame{0}; // Frame actual
|
||||
float accumulated_time{0.0F}; // Tiempo acumulado para las animaciones (time-based)
|
||||
};
|
||||
|
||||
// Métodos estáticos
|
||||
static auto loadAnimationsFromYAML(const std::string& file_path, std::shared_ptr<Surface>& surface, float& frame_width, float& frame_height) -> std::vector<AnimationData>; // Carga las animaciones desde fichero YAML
|
||||
|
||||
// Constructores
|
||||
explicit AnimatedSprite(const AnimationResource& cached_data); // Constructor con datos pre-cargados del cache
|
||||
|
||||
~AnimatedSprite() override = default; // Destructor
|
||||
|
||||
void update(float delta_time) override; // Actualiza las variables del objeto (time-based)
|
||||
|
||||
// Consultas de estado
|
||||
auto animationIsCompleted() -> bool; // Comprueba si ha terminado la animación
|
||||
auto getIndex(const std::string& name) -> int; // Obtiene el índice de la animación por nombre
|
||||
auto getCurrentAnimationSize() -> int { return static_cast<int>(animations_[current_animation_].frames.size()); } // Número de frames de la animación actual
|
||||
|
||||
// Modificadores de animación
|
||||
void setCurrentAnimation(const std::string& name = "default"); // Establece la animación actual por nombre
|
||||
void setCurrentAnimation(int index = 0); // Establece la animación actual por índice
|
||||
void resetAnimation(); // Reinicia la animación
|
||||
void setCurrentAnimationFrame(int num); // Establece el frame actual de la animación
|
||||
void animate(float delta_time); // Calcula el frame correspondiente a la animación actual (time-based)
|
||||
|
||||
protected:
|
||||
// Constructor per a ús de subclasses que gestionen la surface directament (sense YAML)
|
||||
AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||
|
||||
private:
|
||||
// Variables miembro
|
||||
std::vector<AnimationData> animations_; // Vector con las diferentes animaciones
|
||||
int current_animation_{0}; // Animación activa
|
||||
};
|
||||
188
source/core/rendering/sprite/dissolve_sprite.cpp
Normal file
188
source/core/rendering/sprite/dissolve_sprite.cpp
Normal file
@@ -0,0 +1,188 @@
|
||||
#include "core/rendering/sprite/dissolve_sprite.hpp"
|
||||
|
||||
#include <algorithm> // Para min
|
||||
#include <cstdint> // Para uint32_t
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
|
||||
// Hash 2D estable per a dithering (rank aleatori per posició de píxel)
|
||||
static auto pixelRank(int col, int row) -> float {
|
||||
auto h = (static_cast<uint32_t>(col) * 2246822519U) ^ (static_cast<uint32_t>(row) * 2654435761U);
|
||||
h ^= (h >> 13);
|
||||
h *= 1274126177U;
|
||||
h ^= (h >> 16);
|
||||
return static_cast<float>(h & 0xFFFFU) / 65536.0F;
|
||||
}
|
||||
|
||||
// Rang per a un píxel tenint en compte direcció (70% direccional + 30% aleatori)
|
||||
auto DissolveSprite::computePixelRank(int col, int row, int frame_h, DissolveDirection dir) -> float {
|
||||
const float RANDOM = pixelRank(col, row);
|
||||
if (dir == DissolveDirection::NONE || frame_h <= 0) {
|
||||
return RANDOM;
|
||||
}
|
||||
|
||||
float y_factor = 0.0F;
|
||||
if (dir == DissolveDirection::DOWN) {
|
||||
y_factor = static_cast<float>(row) / static_cast<float>(frame_h);
|
||||
} else {
|
||||
y_factor = static_cast<float>(frame_h - 1 - row) / static_cast<float>(frame_h);
|
||||
}
|
||||
|
||||
return (y_factor * 0.7F) + (RANDOM * 0.3F);
|
||||
}
|
||||
|
||||
// Constructor per a surface directa (sense AnimationResource)
|
||||
DissolveSprite::DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||
: AnimatedSprite(std::move(surface), pos) {
|
||||
if (surface_) {
|
||||
const int W = static_cast<int>(surface_->getWidth());
|
||||
const int H = static_cast<int>(surface_->getHeight());
|
||||
surface_display_ = std::make_shared<Surface>(W, H);
|
||||
surface_display_->setTransparentColor(surface_->getTransparentColor());
|
||||
surface_display_->clear(surface_->getTransparentColor());
|
||||
}
|
||||
}
|
||||
|
||||
// Constructor
|
||||
DissolveSprite::DissolveSprite(const AnimationResource& data)
|
||||
: AnimatedSprite(data) {
|
||||
if (surface_) {
|
||||
const int W = static_cast<int>(surface_->getWidth());
|
||||
const int H = static_cast<int>(surface_->getHeight());
|
||||
surface_display_ = std::make_shared<Surface>(W, H);
|
||||
surface_display_->setTransparentColor(surface_->getTransparentColor());
|
||||
// Inicialitza tots els píxels com a transparents
|
||||
surface_display_->clear(surface_->getTransparentColor());
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstrueix la surface_display_ filtrant píxels per progress_
|
||||
void DissolveSprite::rebuildDisplaySurface() {
|
||||
if (!surface_ || !surface_display_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SDL_FRect CLIP = clip_;
|
||||
const int SX = static_cast<int>(CLIP.x);
|
||||
const int SY = static_cast<int>(CLIP.y);
|
||||
const int SW = static_cast<int>(CLIP.w);
|
||||
const int SH = static_cast<int>(CLIP.h);
|
||||
|
||||
if (SW <= 0 || SH <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto src_data = surface_->getSurfaceData();
|
||||
auto dst_data = surface_display_->getSurfaceData();
|
||||
|
||||
const int SRC_W = static_cast<int>(src_data->width);
|
||||
const int DST_W = static_cast<int>(dst_data->width);
|
||||
const Uint8 TRANSPARENT = surface_->getTransparentColor();
|
||||
|
||||
// Esborra frame anterior si ha canviat
|
||||
if (prev_clip_.w > 0 && prev_clip_.h > 0 &&
|
||||
(prev_clip_.x != CLIP.x || prev_clip_.y != CLIP.y ||
|
||||
prev_clip_.w != CLIP.w || prev_clip_.h != CLIP.h)) {
|
||||
surface_display_->fillRect(&prev_clip_, TRANSPARENT);
|
||||
}
|
||||
|
||||
// Esborra la zona del frame actual (reconstrucció neta)
|
||||
surface_display_->fillRect(&CLIP, TRANSPARENT);
|
||||
|
||||
// Copia píxels filtrats per progress_
|
||||
for (int row = 0; row < SH; ++row) {
|
||||
for (int col = 0; col < SW; ++col) {
|
||||
const Uint8 COLOR = src_data->data[((SY + row) * SRC_W) + (SX + col)];
|
||||
if (COLOR == TRANSPARENT) {
|
||||
continue;
|
||||
}
|
||||
const float RANK = computePixelRank(col, row, SH, direction_);
|
||||
if (RANK >= progress_) {
|
||||
const Uint8 OUT = (COLOR == source_color_) ? target_color_ : COLOR;
|
||||
dst_data->data[((SY + row) * DST_W) + (SX + col)] = OUT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prev_clip_ = CLIP;
|
||||
needs_rebuild_ = false;
|
||||
}
|
||||
|
||||
// Actualitza animació, moviment i transició temporal
|
||||
void DissolveSprite::update(float delta_time) {
|
||||
const SDL_FRect OLD_CLIP = clip_;
|
||||
AnimatedSprite::update(delta_time);
|
||||
|
||||
// Detecta canvi de frame d'animació
|
||||
if (clip_.x != OLD_CLIP.x || clip_.y != OLD_CLIP.y ||
|
||||
clip_.w != OLD_CLIP.w || clip_.h != OLD_CLIP.h) {
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Actualitza transició temporal si activa
|
||||
if (transition_mode_ != TransitionMode::NONE) {
|
||||
transition_elapsed_ += delta_time * 1000.0F;
|
||||
const float T = std::min(transition_elapsed_ / transition_duration_, 1.0F);
|
||||
progress_ = (transition_mode_ == TransitionMode::DISSOLVING) ? T : (1.0F - T);
|
||||
needs_rebuild_ = true;
|
||||
if (T >= 1.0F) {
|
||||
transition_mode_ = TransitionMode::NONE;
|
||||
}
|
||||
}
|
||||
|
||||
if (needs_rebuild_) {
|
||||
rebuildDisplaySurface();
|
||||
}
|
||||
}
|
||||
|
||||
// Renderitza: usa surface_display_ (amb color replace) si disponible
|
||||
void DissolveSprite::render() {
|
||||
if (!surface_display_) {
|
||||
AnimatedSprite::render();
|
||||
return;
|
||||
}
|
||||
surface_display_->render(static_cast<int>(pos_.x), static_cast<int>(pos_.y), &clip_, flip_);
|
||||
}
|
||||
|
||||
// Estableix el progrés manualment
|
||||
void DissolveSprite::setProgress(float progress) {
|
||||
progress_ = std::min(std::max(progress, 0.0F), 1.0F);
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Inicia dissolució temporal (visible → invisible)
|
||||
void DissolveSprite::startDissolve(float duration_ms, DissolveDirection dir) {
|
||||
direction_ = dir;
|
||||
transition_mode_ = TransitionMode::DISSOLVING;
|
||||
transition_duration_ = duration_ms;
|
||||
transition_elapsed_ = 0.0F;
|
||||
progress_ = 0.0F;
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Inicia generació temporal (invisible → visible)
|
||||
void DissolveSprite::startGenerate(float duration_ms, DissolveDirection dir) {
|
||||
direction_ = dir;
|
||||
transition_mode_ = TransitionMode::GENERATING;
|
||||
transition_duration_ = duration_ms;
|
||||
transition_elapsed_ = 0.0F;
|
||||
progress_ = 1.0F;
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
|
||||
// Atura la transició temporal
|
||||
void DissolveSprite::stopTransition() {
|
||||
transition_mode_ = TransitionMode::NONE;
|
||||
}
|
||||
|
||||
// Retorna si la transició ha acabat
|
||||
auto DissolveSprite::isTransitionDone() const -> bool {
|
||||
return transition_mode_ == TransitionMode::NONE;
|
||||
}
|
||||
|
||||
// Configura substitució de color per a la reconstrucció
|
||||
void DissolveSprite::setColorReplace(Uint8 source, Uint8 target) {
|
||||
source_color_ = source;
|
||||
target_color_ = target;
|
||||
needs_rebuild_ = true;
|
||||
}
|
||||
62
source/core/rendering/sprite/dissolve_sprite.hpp
Normal file
62
source/core/rendering/sprite/dissolve_sprite.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
|
||||
#include "core/rendering/sprite/animated_sprite.hpp" // Para SurfaceAnimatedSprite
|
||||
|
||||
class Surface;
|
||||
|
||||
// Direcció de la dissolució
|
||||
enum class DissolveDirection { NONE,
|
||||
DOWN,
|
||||
UP };
|
||||
|
||||
// Sprite que pot dissoldre's o generar-se de forma aleatòria en X mil·lisegons.
|
||||
// progress_ va de 0.0 (totalment visible) a 1.0 (totalment invisible).
|
||||
class DissolveSprite : public AnimatedSprite {
|
||||
public:
|
||||
explicit DissolveSprite(const AnimationResource& data);
|
||||
DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||
~DissolveSprite() override = default;
|
||||
|
||||
void update(float delta_time) override;
|
||||
void render() override;
|
||||
|
||||
// Progrés manual [0.0 = totalment visible, 1.0 = totalment invisible]
|
||||
void setProgress(float progress);
|
||||
[[nodiscard]] auto getProgress() const -> float { return progress_; }
|
||||
|
||||
// Inicia una dissolució temporal (visible → invisible en duration_ms)
|
||||
void startDissolve(float duration_ms, DissolveDirection dir = DissolveDirection::NONE);
|
||||
|
||||
// Inicia una generació temporal (invisible → visible en duration_ms)
|
||||
void startGenerate(float duration_ms, DissolveDirection dir = DissolveDirection::NONE);
|
||||
|
||||
void stopTransition();
|
||||
[[nodiscard]] auto isTransitionDone() const -> bool;
|
||||
|
||||
// Substitució de color: en reconstruir, substitueix source per target
|
||||
void setColorReplace(Uint8 source, Uint8 target);
|
||||
|
||||
private:
|
||||
enum class TransitionMode { NONE,
|
||||
DISSOLVING,
|
||||
GENERATING };
|
||||
|
||||
std::shared_ptr<Surface> surface_display_; // Superfície amb els píxels filtrats
|
||||
|
||||
float progress_{0.0F}; // [0=visible, 1=invisible]
|
||||
DissolveDirection direction_{DissolveDirection::NONE};
|
||||
TransitionMode transition_mode_{TransitionMode::NONE};
|
||||
float transition_duration_{0.0F};
|
||||
float transition_elapsed_{0.0F};
|
||||
SDL_FRect prev_clip_{.x = 0, .y = 0, .w = 0, .h = 0};
|
||||
bool needs_rebuild_{false};
|
||||
Uint8 source_color_{255}; // 255 = transparent = sense replace per defecte
|
||||
Uint8 target_color_{0};
|
||||
|
||||
void rebuildDisplaySurface();
|
||||
[[nodiscard]] static auto computePixelRank(int col, int row, int frame_h, DissolveDirection dir) -> float;
|
||||
};
|
||||
103
source/core/rendering/sprite/moving_sprite.cpp
Normal file
103
source/core/rendering/sprite/moving_sprite.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "core/rendering/sprite/moving_sprite.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
|
||||
// Constructor
|
||||
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip)
|
||||
: Sprite(std::move(surface), pos),
|
||||
x_(pos.x),
|
||||
y_(pos.y),
|
||||
flip_(flip) { Sprite::pos_ = pos; }
|
||||
|
||||
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
|
||||
: Sprite(std::move(surface), pos),
|
||||
x_(pos.x),
|
||||
y_(pos.y) { Sprite::pos_ = pos; }
|
||||
|
||||
MovingSprite::MovingSprite() { Sprite::clear(); }
|
||||
|
||||
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface)
|
||||
: Sprite(std::move(surface)) { Sprite::clear(); }
|
||||
|
||||
// Reinicia todas las variables
|
||||
void MovingSprite::clear() {
|
||||
// Resetea posición
|
||||
x_ = 0.0F;
|
||||
y_ = 0.0F;
|
||||
|
||||
// Resetea velocidad
|
||||
vx_ = 0.0F;
|
||||
vy_ = 0.0F;
|
||||
|
||||
// Resetea aceleración
|
||||
ax_ = 0.0F;
|
||||
ay_ = 0.0F;
|
||||
|
||||
// Resetea flip
|
||||
flip_ = SDL_FLIP_NONE;
|
||||
|
||||
Sprite::clear();
|
||||
}
|
||||
|
||||
// Mueve el sprite (time-based)
|
||||
// Nota: vx_, vy_ ahora se interpretan como pixels/segundo
|
||||
// Nota: ax_, ay_ ahora se interpretan como pixels/segundo²
|
||||
void MovingSprite::move(float delta_time) {
|
||||
// Aplica aceleración a velocidad (time-based)
|
||||
vx_ += ax_ * delta_time;
|
||||
vy_ += ay_ * delta_time;
|
||||
|
||||
// Aplica velocidad a posición (time-based)
|
||||
x_ += vx_ * delta_time;
|
||||
y_ += vy_ * delta_time;
|
||||
|
||||
// Actualiza posición entera para renderizado
|
||||
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);
|
||||
}
|
||||
|
||||
// Muestra el sprite por pantalla
|
||||
void MovingSprite::render() {
|
||||
surface_->render(pos_.x, pos_.y, &clip_, flip_);
|
||||
}
|
||||
|
||||
// Muestra el sprite por pantalla
|
||||
void MovingSprite::render(Uint8 source_color, Uint8 target_color) {
|
||||
surface_->renderWithColorReplace(pos_.x, pos_.y, source_color, target_color, &clip_, flip_);
|
||||
}
|
||||
|
||||
// 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 x, float y) {
|
||||
x_ = x;
|
||||
y_ = y;
|
||||
|
||||
pos_.x = static_cast<int>(x_);
|
||||
pos_.y = static_cast<int>(y_);
|
||||
}
|
||||
|
||||
// Establece el valor de la variable
|
||||
void MovingSprite::setPosX(float value) {
|
||||
x_ = value;
|
||||
pos_.x = static_cast<int>(x_);
|
||||
}
|
||||
|
||||
// Establece el valor de la variable
|
||||
void MovingSprite::setPosY(float value) {
|
||||
y_ = value;
|
||||
pos_.y = static_cast<int>(y_);
|
||||
}
|
||||
77
source/core/rendering/sprite/moving_sprite.hpp
Normal file
77
source/core/rendering/sprite/moving_sprite.hpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
class Surface; // lines 8-8
|
||||
|
||||
// Clase SMovingSprite. Añade movimiento y flip al sprite
|
||||
class MovingSprite : public Sprite {
|
||||
public:
|
||||
// Constructores
|
||||
MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip);
|
||||
MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
|
||||
explicit MovingSprite();
|
||||
explicit MovingSprite(std::shared_ptr<Surface> surface);
|
||||
~MovingSprite() override = default;
|
||||
|
||||
// Actualización y renderizado
|
||||
void update(float delta_time) override; // Actualiza variables internas (time-based)
|
||||
void render() override; // Muestra el sprite por pantalla
|
||||
void render(Uint8 source_color, Uint8 target_color) override; // Renderiza con reemplazo de color
|
||||
|
||||
// Gestión de estado
|
||||
void clear() override; // Reinicia todas las variables a cero
|
||||
|
||||
// Getters de posición
|
||||
[[nodiscard]] auto getPosX() const -> float { return x_; }
|
||||
[[nodiscard]] auto getPosY() const -> float { return y_; }
|
||||
|
||||
// Getters de velocidad
|
||||
[[nodiscard]] auto getVelX() const -> float { return vx_; }
|
||||
[[nodiscard]] auto getVelY() const -> float { return vy_; }
|
||||
|
||||
// Getters de aceleración
|
||||
[[nodiscard]] auto getAccelX() const -> float { return ax_; }
|
||||
[[nodiscard]] auto getAccelY() const -> float { return ay_; }
|
||||
|
||||
// Setters de posición
|
||||
void setPos(SDL_FRect rect); // Establece posición y tamaño del objeto
|
||||
void setPos(float x, float y); // Establece posición x, y
|
||||
void setPosX(float value); // Establece posición X
|
||||
void setPosY(float value); // Establece posición Y
|
||||
|
||||
// Setters de velocidad
|
||||
void setVelX(float value) { vx_ = value; }
|
||||
void setVelY(float value) { vy_ = value; }
|
||||
|
||||
// Setters de aceleración
|
||||
void setAccelX(float value) { ax_ = value; }
|
||||
void setAccelY(float value) { ay_ = value; }
|
||||
|
||||
// Gestión de flip (volteo horizontal)
|
||||
void setFlip(SDL_FlipMode flip) { flip_ = flip; } // Establece modo de flip
|
||||
auto getFlip() -> SDL_FlipMode { return flip_; } // Obtiene modo de flip
|
||||
void flip() { flip_ = (flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL; } // Alterna flip horizontal
|
||||
|
||||
protected:
|
||||
// Métodos protegidos
|
||||
void move(float delta_time); // Mueve el sprite (time-based)
|
||||
|
||||
// Variables miembro - Posición
|
||||
float x_{0.0F}; // Posición en el eje X
|
||||
float y_{0.0F}; // Posición en el eje Y
|
||||
|
||||
// Variables miembro - Velocidad (pixels/segundo)
|
||||
float vx_{0.0F}; // Velocidad en el eje X
|
||||
float vy_{0.0F}; // Velocidad en el eje Y
|
||||
|
||||
// Variables miembro - Aceleración (pixels/segundo²)
|
||||
float ax_{0.0F}; // Aceleración en el eje X
|
||||
float ay_{0.0F}; // Aceleración en el eje Y
|
||||
|
||||
// Variables miembro - Renderizado
|
||||
SDL_FlipMode flip_{SDL_FLIP_NONE}; // Modo de volteo del sprite
|
||||
};
|
||||
76
source/core/rendering/sprite/sprite.cpp
Normal file
76
source/core/rendering/sprite/sprite.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "core/rendering/sprite/sprite.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
|
||||
// Constructor
|
||||
Sprite::Sprite(std::shared_ptr<Surface> surface, float x, float y, float w, float h)
|
||||
: surface_(std::move(surface)),
|
||||
pos_{.x = x, .y = y, .w = w, .h = h},
|
||||
clip_{.x = 0.0F, .y = 0.0F, .w = pos_.w, .h = pos_.h} {}
|
||||
|
||||
Sprite::Sprite(std::shared_ptr<Surface> surface, SDL_FRect rect)
|
||||
: surface_(std::move(surface)),
|
||||
pos_(rect),
|
||||
clip_{.x = 0.0F, .y = 0.0F, .w = pos_.w, .h = pos_.h} {}
|
||||
|
||||
Sprite::Sprite() = default;
|
||||
|
||||
Sprite::Sprite(std::shared_ptr<Surface> surface)
|
||||
: surface_(std::move(surface)),
|
||||
pos_{0.0F, 0.0F, surface_->getWidth(), surface_->getHeight()},
|
||||
clip_(pos_) {}
|
||||
|
||||
// Muestra el sprite por pantalla
|
||||
void Sprite::render() {
|
||||
surface_->render(pos_.x, pos_.y, &clip_);
|
||||
}
|
||||
|
||||
void Sprite::render(Uint8 source_color, Uint8 target_color) {
|
||||
surface_->renderWithColorReplace(pos_.x, pos_.y, source_color, target_color, &clip_);
|
||||
}
|
||||
|
||||
void Sprite::renderWithVerticalFade(int fade_h, int canvas_height) {
|
||||
surface_->renderWithVerticalFade(
|
||||
static_cast<int>(pos_.x),
|
||||
static_cast<int>(pos_.y),
|
||||
fade_h,
|
||||
canvas_height,
|
||||
&clip_);
|
||||
}
|
||||
|
||||
void Sprite::renderWithVerticalFade(int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color) {
|
||||
surface_->renderWithVerticalFade(
|
||||
static_cast<int>(pos_.x),
|
||||
static_cast<int>(pos_.y),
|
||||
fade_h,
|
||||
canvas_height,
|
||||
source_color,
|
||||
target_color,
|
||||
&clip_);
|
||||
}
|
||||
|
||||
// Establece la posición del objeto
|
||||
void Sprite::setPosition(float x, float y) {
|
||||
pos_.x = x;
|
||||
pos_.y = y;
|
||||
}
|
||||
|
||||
// Establece la posición del objeto
|
||||
void Sprite::setPosition(SDL_FPoint p) {
|
||||
pos_.x = p.x;
|
||||
pos_.y = p.y;
|
||||
}
|
||||
|
||||
// Reinicia las variables a cero
|
||||
void Sprite::clear() {
|
||||
pos_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
|
||||
clip_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
|
||||
}
|
||||
|
||||
// Actualiza el estado del sprite (time-based)
|
||||
void Sprite::update(float delta_time) {
|
||||
// Base implementation does nothing (static sprites)
|
||||
(void)delta_time; // Evita warning de parámetro no usado
|
||||
}
|
||||
62
source/core/rendering/sprite/sprite.hpp
Normal file
62
source/core/rendering/sprite/sprite.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <utility>
|
||||
class Surface; // lines 5-5
|
||||
|
||||
// Clase SurfaceSprite
|
||||
class Sprite {
|
||||
public:
|
||||
// Constructores
|
||||
Sprite(std::shared_ptr<Surface>, float x, float y, float w, float h);
|
||||
Sprite(std::shared_ptr<Surface>, SDL_FRect rect);
|
||||
Sprite();
|
||||
explicit Sprite(std::shared_ptr<Surface>);
|
||||
|
||||
// Destructor
|
||||
virtual ~Sprite() = default;
|
||||
|
||||
// Actualización y renderizado
|
||||
virtual void update(float delta_time); // Actualiza el estado del sprite (time-based)
|
||||
virtual void render(); // Muestra el sprite por pantalla
|
||||
virtual void render(Uint8 source_color, Uint8 target_color); // Renderiza con reemplazo de color
|
||||
virtual void renderWithVerticalFade(int fade_h, int canvas_height); // Renderiza amb dissolució vertical (hash 2D, sense parpelleig)
|
||||
virtual void renderWithVerticalFade(int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color); // Idem amb reemplaç de color
|
||||
|
||||
// Gestión de estado
|
||||
virtual void clear(); // Reinicia las variables a cero
|
||||
|
||||
// Obtención de propiedades
|
||||
[[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_; }
|
||||
[[nodiscard]] auto getClip() const -> SDL_FRect { return clip_; }
|
||||
[[nodiscard]] auto getSurface() const -> std::shared_ptr<Surface> { return surface_; }
|
||||
auto getRect() -> SDL_FRect& { return pos_; }
|
||||
|
||||
// Modificación de posición y tamaño
|
||||
void setX(float x) { pos_.x = x; }
|
||||
void setY(float y) { pos_.y = y; }
|
||||
void setWidth(float w) { pos_.w = w; }
|
||||
void setHeight(float h) { pos_.h = h; }
|
||||
void setPosition(float x, float y);
|
||||
void setPosition(SDL_FPoint p);
|
||||
void setPosition(SDL_FRect r) { pos_ = r; }
|
||||
void incX(float value) { pos_.x += value; }
|
||||
void incY(float value) { pos_.y += value; }
|
||||
|
||||
// Modificación de clip y surface
|
||||
void setClip(SDL_FRect rect) { clip_ = rect; }
|
||||
void setClip(float x, float y, float w, float h) { clip_ = SDL_FRect{.x = x, .y = y, .w = w, .h = h}; }
|
||||
void setSurface(std::shared_ptr<Surface> surface) { surface_ = std::move(surface); }
|
||||
|
||||
protected:
|
||||
// Variables miembro
|
||||
std::shared_ptr<Surface> surface_{nullptr}; // Surface donde estan todos los dibujos del sprite
|
||||
SDL_FRect pos_{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; // Posición y tamaño donde dibujar el sprite
|
||||
SDL_FRect clip_{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; // Rectangulo de origen de la surface que se dibujará en pantalla
|
||||
};
|
||||
695
source/core/rendering/surface.cpp
Normal file
695
source/core/rendering/surface.cpp
Normal file
@@ -0,0 +1,695 @@
|
||||
// IWYU pragma: no_include <bits/std_abs.h>
|
||||
#include "core/rendering/surface.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para min, max, copy_n, fill
|
||||
#include <cmath> // Para abs
|
||||
#include <cstdint> // Para uint32_t
|
||||
#include <cstring> // Para memcpy, size_t
|
||||
#include <fstream> // Para basic_ifstream, basic_ostream, basic_ist...
|
||||
#include <iostream> // Para cerr
|
||||
#include <memory> // Para shared_ptr, __shared_ptr_access, default...
|
||||
#include <sstream> // Para basic_istringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/gif.hpp" // Para Gif
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
|
||||
// Carga una paleta desde un archivo .gif
|
||||
auto loadPalette(const std::string& file_path) -> Palette {
|
||||
// Load file using ResourceHelper (supports both filesystem and pack)
|
||||
auto buffer = Resource::Helper::loadFile(file_path);
|
||||
if (buffer.empty()) {
|
||||
throw std::runtime_error("Error opening file: " + file_path);
|
||||
}
|
||||
|
||||
// Cargar la paleta usando los datos del buffer
|
||||
std::vector<uint32_t> pal = GIF::Gif::loadPalette(buffer.data());
|
||||
if (pal.empty()) {
|
||||
throw std::runtime_error("No palette found in GIF file: " + file_path);
|
||||
}
|
||||
|
||||
// Crear la paleta y copiar los datos desde 'pal'
|
||||
Palette palette = {}; // Inicializa la paleta con ceros
|
||||
std::copy_n(pal.begin(), std::min(pal.size(), palette.size()), palette.begin());
|
||||
|
||||
// Mensaje de depuración
|
||||
printWithDots("Palette : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||
|
||||
return palette;
|
||||
}
|
||||
|
||||
// Carga una paleta desde un archivo .pal
|
||||
auto readPalFile(const std::string& file_path) -> Palette {
|
||||
Palette palette{};
|
||||
palette.fill(0); // Inicializar todo con 0 (transparente por defecto)
|
||||
|
||||
// Load file using ResourceHelper (supports both filesystem and pack)
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
if (file_data.empty()) {
|
||||
throw std::runtime_error("No se pudo abrir el archivo .pal: " + file_path);
|
||||
}
|
||||
|
||||
// Convert bytes to string for parsing
|
||||
std::string content(file_data.begin(), file_data.end());
|
||||
std::istringstream stream(content);
|
||||
|
||||
std::string line;
|
||||
int line_number = 0;
|
||||
int color_index = 0;
|
||||
|
||||
while (std::getline(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 ARGB (A = 255 por defecto)
|
||||
Uint32 color = (255 << 24) | (r << 16) | (g << 8) | b;
|
||||
palette[color_index++] = color;
|
||||
|
||||
// Limitar a un máximo de 256 colores (opcional)
|
||||
if (color_index >= 256) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printWithDots("Palette : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||
return palette;
|
||||
}
|
||||
|
||||
// Constructor
|
||||
Surface::Surface(int w, int h)
|
||||
: surface_data_(std::make_shared<SurfaceData>(w, h)),
|
||||
transparent_color_(static_cast<Uint8>(PaletteColor::TRANSPARENT)) { initializeSubPalette(sub_palette_); }
|
||||
|
||||
Surface::Surface(const std::string& file_path)
|
||||
: transparent_color_(static_cast<Uint8>(PaletteColor::TRANSPARENT)) {
|
||||
SurfaceData loaded_data = loadSurface(file_path);
|
||||
surface_data_ = std::make_shared<SurfaceData>(std::move(loaded_data));
|
||||
|
||||
initializeSubPalette(sub_palette_);
|
||||
}
|
||||
|
||||
// Carga una superficie desde un archivo
|
||||
auto Surface::loadSurface(const std::string& file_path) -> SurfaceData { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Load file using ResourceHelper (supports both filesystem and pack)
|
||||
std::vector<Uint8> buffer = Resource::Helper::loadFile(file_path);
|
||||
if (buffer.empty()) {
|
||||
std::cerr << "Error opening file: " << file_path << '\n';
|
||||
throw std::runtime_error("Error opening file");
|
||||
}
|
||||
|
||||
// Crear un objeto Gif y llamar a la función loadGif
|
||||
Uint16 w = 0;
|
||||
Uint16 h = 0;
|
||||
std::vector<Uint8> raw_pixels = GIF::Gif::loadGif(buffer.data(), w, h);
|
||||
if (raw_pixels.empty()) {
|
||||
std::cerr << "Error loading GIF from file: " << file_path << '\n';
|
||||
throw std::runtime_error("Error loading GIF");
|
||||
}
|
||||
|
||||
// Si el constructor de Surface espera un std::shared_ptr<Uint8[]>,
|
||||
// reservamos un bloque dinámico y copiamos los datos del vector.
|
||||
size_t pixel_count = raw_pixels.size();
|
||||
auto pixels = std::shared_ptr<Uint8[]>(new Uint8[pixel_count], std::default_delete<Uint8[]>());
|
||||
std::memcpy(pixels.get(), raw_pixels.data(), pixel_count);
|
||||
|
||||
// Crear y devolver directamente el objeto SurfaceData
|
||||
printWithDots("Surface : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
|
||||
return {static_cast<float>(w), static_cast<float>(h), pixels};
|
||||
}
|
||||
|
||||
// Carga una paleta desde un archivo
|
||||
void Surface::loadPalette(const std::string& file_path) {
|
||||
palette_ = ::loadPalette(file_path);
|
||||
}
|
||||
|
||||
// Carga una paleta desde otra paleta
|
||||
void Surface::loadPalette(const Palette& palette) {
|
||||
palette_ = palette;
|
||||
}
|
||||
|
||||
// Establece un color en la paleta
|
||||
void Surface::setColor(int index, Uint32 color) {
|
||||
palette_.at(index) = color;
|
||||
}
|
||||
|
||||
// Rellena la superficie con un color
|
||||
void Surface::clear(Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const size_t TOTAL_PIXELS = surface_data_->width * surface_data_->height;
|
||||
Uint8* data_ptr = surface_data_->data.get();
|
||||
std::fill(data_ptr, data_ptr + TOTAL_PIXELS, color);
|
||||
}
|
||||
|
||||
// Pone un pixel en la SurfaceData
|
||||
void Surface::putPixel(int x, int y, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (x < 0 || y < 0 || x >= surface_data_->width || y >= surface_data_->height) {
|
||||
return; // Coordenadas fuera de rango
|
||||
}
|
||||
|
||||
const int INDEX = x + (y * surface_data_->width);
|
||||
surface_data_->data.get()[INDEX] = color;
|
||||
}
|
||||
|
||||
// Obtiene el color de un pixel de la surface_data
|
||||
auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * static_cast<int>(surface_data_->width))]; }
|
||||
|
||||
// Dibuja un rectangulo relleno
|
||||
void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Limitar los valores del rectángulo al tamaño de la superficie
|
||||
float x_start = std::max(0.0F, rect->x);
|
||||
float y_start = std::max(0.0F, rect->y);
|
||||
float x_end = std::min(rect->x + rect->w, surface_data_->width);
|
||||
float y_end = std::min(rect->y + rect->h, surface_data_->height);
|
||||
|
||||
// Rellenar fila a fila con memset (memoria contigua por fila)
|
||||
Uint8* data_ptr = surface_data_->data.get();
|
||||
const int SURF_WIDTH = static_cast<int>(surface_data_->width);
|
||||
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
|
||||
for (int y = static_cast<int>(y_start); y < static_cast<int>(y_end); ++y) {
|
||||
std::memset(data_ptr + (y * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja el borde de un rectangulo
|
||||
void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Limitar los valores del rectángulo al tamaño de la superficie
|
||||
float x_start = std::max(0.0F, rect->x);
|
||||
float y_start = std::max(0.0F, rect->y);
|
||||
float x_end = std::min(rect->x + rect->w, surface_data_->width);
|
||||
float y_end = std::min(rect->y + rect->h, surface_data_->height);
|
||||
|
||||
// Dibujar bordes horizontales con memset (líneas contiguas en memoria)
|
||||
Uint8* data_ptr = surface_data_->data.get();
|
||||
const int SURF_WIDTH = static_cast<int>(surface_data_->width);
|
||||
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
|
||||
std::memset(data_ptr + (static_cast<int>(y_start) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
|
||||
std::memset(data_ptr + ((static_cast<int>(y_end) - 1) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
|
||||
|
||||
// Dibujar bordes verticales
|
||||
for (int y = y_start; y < y_end; ++y) {
|
||||
// Borde izquierdo
|
||||
const int LEFT_INDEX = x_start + (y * surface_data_->width);
|
||||
surface_data_->data.get()[LEFT_INDEX] = color;
|
||||
|
||||
// Borde derecho
|
||||
const int RIGHT_INDEX = (x_end - 1) + (y * surface_data_->width);
|
||||
surface_data_->data.get()[RIGHT_INDEX] = color;
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja una linea
|
||||
void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Calcula las diferencias
|
||||
float dx = std::abs(x2 - x1);
|
||||
float dy = std::abs(y2 - y1);
|
||||
|
||||
// Determina la dirección del incremento
|
||||
float sx = (x1 < x2) ? 1 : -1;
|
||||
float sy = (y1 < y2) ? 1 : -1;
|
||||
|
||||
float err = dx - dy;
|
||||
|
||||
while (true) {
|
||||
// Asegúrate de no dibujar fuera de los límites de la superficie
|
||||
if (x1 >= 0 && x1 < surface_data_->width && y1 >= 0 && y1 < surface_data_->height) {
|
||||
surface_data_->data.get()[static_cast<size_t>(x1 + (y1 * surface_data_->width))] = color;
|
||||
}
|
||||
|
||||
// Si alcanzamos el punto final, salimos
|
||||
if (x1 == x2 && y1 == y2) {
|
||||
break;
|
||||
}
|
||||
|
||||
int e2 = 2 * err;
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
x1 += sx;
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
y1 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Surface::render(float dx, float dy, float sx, float sy, float w, float h) { // NOLINT(readability-make-member-function-const)
|
||||
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en origen
|
||||
w = std::min(w, surface_data_->width - sx);
|
||||
h = std::min(h, surface_data_->height - sy);
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en destino
|
||||
w = std::min(w, surface_data->width - dx);
|
||||
h = std::min(h, surface_data->height - dy);
|
||||
|
||||
const Uint8* src_ptr = surface_data_->data.get();
|
||||
Uint8* dst_ptr = surface_data->data.get();
|
||||
for (int iy = 0; iy < h; ++iy) {
|
||||
for (int ix = 0; ix < w; ++ix) {
|
||||
// Verificar que las coordenadas de destino están dentro de los límites
|
||||
if (int dest_x = dx + ix; dest_x >= 0 && dest_x < surface_data->width) {
|
||||
if (int dest_y = dy + iy; dest_y >= 0 && dest_y < surface_data->height) {
|
||||
int src_x = sx + ix;
|
||||
int src_y = sy + iy;
|
||||
|
||||
Uint8 color = src_ptr[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
dst_ptr[static_cast<size_t>(dest_x + (dest_y * surface_data->width))] = sub_palette_[color];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { // NOLINT(readability-make-member-function-const)
|
||||
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Determina la región de origen (clip) a renderizar
|
||||
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||
float w = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||
float h = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en origen
|
||||
w = std::min(w, surface_data_->width - sx);
|
||||
h = std::min(h, surface_data_->height - sy);
|
||||
w = std::min(w, surface_data_dest->width - x);
|
||||
h = std::min(h, surface_data_dest->height - y);
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en destino
|
||||
w = std::min(w, surface_data_dest->width - x);
|
||||
h = std::min(h, surface_data_dest->height - y);
|
||||
|
||||
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||
const Uint8* src_ptr = surface_data_->data.get();
|
||||
Uint8* dst_ptr = surface_data_dest->data.get();
|
||||
for (int iy = 0; iy < h; ++iy) {
|
||||
for (int ix = 0; ix < w; ++ix) {
|
||||
// Coordenadas de origen
|
||||
int src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||
int src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||
|
||||
// Coordenadas de destino
|
||||
int dest_x = x + ix;
|
||||
int dest_y = y + iy;
|
||||
|
||||
// Verificar que las coordenadas de destino están dentro de los límites
|
||||
if (dest_x >= 0 && dest_x < surface_data_dest->width && dest_y >= 0 && dest_y < surface_data_dest->height) {
|
||||
// Copia el píxel si no es transparente
|
||||
Uint8 color = src_ptr[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
dst_ptr[static_cast<size_t>(dest_x + (dest_y * surface_data_dest->width))] = sub_palette_[color];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper para calcular coordenadas con flip
|
||||
void Surface::calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y) {
|
||||
src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||
src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||
}
|
||||
|
||||
// Helper para copiar un pixel si no es transparente
|
||||
void Surface::copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const {
|
||||
if (dest_x < 0 || dest_y < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
dest_data[dest_x + (dest_y * dest_width)] = sub_palette_[color];
|
||||
}
|
||||
}
|
||||
|
||||
// Copia una región de la superficie de origen a la de destino
|
||||
void Surface::render(SDL_FRect* src_rect, SDL_FRect* dst_rect, SDL_FlipMode flip) {
|
||||
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Si srcRect es nullptr, tomar toda la superficie fuente
|
||||
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||
float sw = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||
float sh = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||
|
||||
// Si dstRect es nullptr, asignar las mismas dimensiones que srcRect
|
||||
float dx = (dst_rect != nullptr) ? dst_rect->x : 0;
|
||||
float dy = (dst_rect != nullptr) ? dst_rect->y : 0;
|
||||
float dw = (dst_rect != nullptr) ? dst_rect->w : sw;
|
||||
float dh = (dst_rect != nullptr) ? dst_rect->h : sh;
|
||||
|
||||
// Asegurarse de que srcRect y dstRect tienen las mismas dimensiones
|
||||
if (sw != dw || sh != dh) {
|
||||
dw = sw; // Respetar las dimensiones de srcRect
|
||||
dh = sh;
|
||||
}
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango en src y dst
|
||||
sw = std::min(sw, surface_data_->width - sx);
|
||||
sh = std::min(sh, surface_data_->height - sy);
|
||||
dw = std::min(dw, surface_data->width - dx);
|
||||
dh = std::min(dh, surface_data->height - dy);
|
||||
|
||||
int final_width = std::min(sw, dw);
|
||||
int final_height = std::min(sh, dh);
|
||||
|
||||
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||
for (int iy = 0; iy < final_height; ++iy) {
|
||||
for (int ix = 0; ix < final_width; ++ix) {
|
||||
int src_x = 0;
|
||||
int src_y = 0;
|
||||
calculateFlippedCoords(ix, iy, sx, sy, final_width, final_height, flip, src_x, src_y);
|
||||
|
||||
int dest_x = dx + ix;
|
||||
int dest_y = dy + iy;
|
||||
|
||||
// Verificar límites de destino antes de copiar
|
||||
if (dest_x >= 0 && dest_x < surface_data->width && dest_y >= 0 && dest_y < surface_data->height) {
|
||||
copyPixelIfNotTransparent(surface_data->data.get(), dest_x, dest_y, surface_data->width, src_x, src_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copia una región de la SurfaceData de origen a la SurfaceData de destino reemplazando un color por otro
|
||||
void Surface::renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect, SDL_FlipMode flip) const {
|
||||
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Determina la región de origen (clip) a renderizar
|
||||
float sx = (src_rect != nullptr) ? src_rect->x : 0;
|
||||
float sy = (src_rect != nullptr) ? src_rect->y : 0;
|
||||
float w = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
|
||||
float h = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
|
||||
|
||||
// Limitar la región para evitar accesos fuera de rango
|
||||
w = std::min(w, surface_data_->width - sx);
|
||||
h = std::min(h, surface_data_->height - sy);
|
||||
|
||||
// Renderiza píxel por píxel aplicando el flip si es necesario
|
||||
for (int iy = 0; iy < h; ++iy) {
|
||||
for (int ix = 0; ix < w; ++ix) {
|
||||
// Coordenadas de origen
|
||||
int src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
|
||||
int src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
|
||||
|
||||
// Coordenadas de destino
|
||||
int dest_x = x + ix;
|
||||
int dest_y = y + iy;
|
||||
|
||||
// Verifica que las coordenadas de destino estén dentro de los límites
|
||||
if (dest_x < 0 || dest_y < 0 || dest_x >= surface_data->width || dest_y >= surface_data->height) {
|
||||
continue; // Saltar píxeles fuera del rango del destino
|
||||
}
|
||||
|
||||
// Copia el píxel si no es transparente
|
||||
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
|
||||
if (color != static_cast<Uint8>(transparent_color_)) {
|
||||
surface_data->data[dest_x + (dest_y * surface_data->width)] =
|
||||
(color == source_color) ? target_color : color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hash 2D estable per a dithering sense flickering
|
||||
static auto pixelThreshold(int col, int row) -> float {
|
||||
auto h = (static_cast<uint32_t>(col) * 2246822519U) ^ (static_cast<uint32_t>(row) * 2654435761U);
|
||||
h ^= (h >> 13);
|
||||
h *= 1274126177U;
|
||||
h ^= (h >> 16);
|
||||
return static_cast<float>(h & 0xFFFFU) / 65536.0F;
|
||||
}
|
||||
|
||||
// Calcula la densidad de fade para un pixel en posición screen_y
|
||||
static auto computeFadeDensity(int screen_y, int fade_h, int canvas_height) -> float {
|
||||
if (screen_y < fade_h) {
|
||||
return static_cast<float>(fade_h - screen_y) / static_cast<float>(fade_h);
|
||||
}
|
||||
if (screen_y >= canvas_height - fade_h) {
|
||||
return static_cast<float>(screen_y - (canvas_height - fade_h)) / static_cast<float>(fade_h);
|
||||
}
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
// Render amb dissolució als cantons superior/inferior (hash 2D, sense parpelleig)
|
||||
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect) const {
|
||||
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
|
||||
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
|
||||
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width);
|
||||
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
|
||||
|
||||
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
for (int row = 0; row < SH; row++) {
|
||||
const int SCREEN_Y = y + row;
|
||||
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DENSITY = computeFadeDensity(SCREEN_Y, fade_h, canvas_height);
|
||||
|
||||
for (int col = 0; col < SW; col++) {
|
||||
const int SCREEN_X = x + col;
|
||||
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
|
||||
if (COLOR == static_cast<Uint8>(transparent_color_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pixelThreshold(col, row) < DENSITY) {
|
||||
continue; // Pixel tapat per la zona de fade
|
||||
}
|
||||
|
||||
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = sub_palette_[COLOR];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Idem però reemplaçant un color índex
|
||||
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect) const {
|
||||
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
|
||||
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
|
||||
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width);
|
||||
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
|
||||
|
||||
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
for (int row = 0; row < SH; row++) {
|
||||
const int SCREEN_Y = y + row;
|
||||
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float DENSITY = computeFadeDensity(SCREEN_Y, fade_h, canvas_height);
|
||||
|
||||
for (int col = 0; col < SW; col++) {
|
||||
const int SCREEN_X = x + col;
|
||||
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
|
||||
if (COLOR == static_cast<Uint8>(transparent_color_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pixelThreshold(col, row) < DENSITY) {
|
||||
continue; // Pixel tapat per la zona de fade
|
||||
}
|
||||
|
||||
const Uint8 OUT_COLOR = (COLOR == source_color) ? target_color : sub_palette_[COLOR];
|
||||
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = OUT_COLOR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vuelca los píxeles como ARGB8888 a un buffer externo (sin SDL_Texture ni SDL_Renderer)
|
||||
void Surface::toARGBBuffer(Uint32* buffer) const {
|
||||
if (!surface_data_ || !surface_data_->data || (buffer == nullptr)) { return; }
|
||||
|
||||
const int WIDTH = static_cast<int>(surface_data_->width);
|
||||
const int HEIGHT = static_cast<int>(surface_data_->height);
|
||||
const Uint8* src = surface_data_->data.get();
|
||||
|
||||
// Obtenemos el tamaño de la paleta para evitar accesos fuera de rango
|
||||
const size_t PAL_SIZE = palette_.size();
|
||||
|
||||
for (int i = 0; i < WIDTH * HEIGHT; ++i) {
|
||||
Uint8 color_index = src[i];
|
||||
|
||||
// Verificación de seguridad: ¿El índice existe en la paleta?
|
||||
if (color_index < PAL_SIZE) {
|
||||
buffer[i] = palette_[color_index];
|
||||
} else {
|
||||
buffer[i] = 0xFF000000; // Negro opaco si el índice es erróneo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vuelca la superficie a una textura
|
||||
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||
throw std::runtime_error("Renderer or texture is null.");
|
||||
}
|
||||
|
||||
if (surface_data_->width <= 0 || surface_data_->height <= 0 || (surface_data_->data == nullptr)) {
|
||||
throw std::runtime_error("Invalid surface dimensions or data.");
|
||||
}
|
||||
|
||||
Uint32* pixels = nullptr;
|
||||
int pitch = 0;
|
||||
|
||||
// Bloquea la textura para modificar los píxeles directamente
|
||||
if (!SDL_LockTexture(texture, nullptr, reinterpret_cast<void**>(&pixels), &pitch)) {
|
||||
throw std::runtime_error("Failed to lock texture: " + std::string(SDL_GetError()));
|
||||
}
|
||||
|
||||
// Convertir `pitch` de bytes a Uint32 (asegurando alineación correcta en hardware)
|
||||
int row_stride = pitch / sizeof(Uint32);
|
||||
|
||||
// Cachear punteros fuera del bucle para permitir autovectorización SIMD
|
||||
const Uint8* src = surface_data_->data.get();
|
||||
const Uint32* pal = palette_.data();
|
||||
const int WIDTH = surface_data_->width;
|
||||
const int HEIGHT = surface_data_->height;
|
||||
for (int y = 0; y < HEIGHT; ++y) {
|
||||
const Uint8* src_row = src + (y * WIDTH);
|
||||
Uint32* dst_row = pixels + (y * row_stride);
|
||||
for (int x = 0; x < WIDTH; ++x) {
|
||||
dst_row[x] = pal[src_row[x]];
|
||||
}
|
||||
}
|
||||
|
||||
SDL_UnlockTexture(texture); // Desbloquea la textura
|
||||
|
||||
// Renderiza la textura en la pantalla completa
|
||||
if (!SDL_RenderTexture(renderer, texture, nullptr, nullptr)) {
|
||||
throw std::runtime_error("Failed to copy texture to renderer: " + std::string(SDL_GetError()));
|
||||
}
|
||||
}
|
||||
|
||||
// Vuelca la superficie a una textura
|
||||
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||
throw std::runtime_error("Renderer or texture is null.");
|
||||
}
|
||||
|
||||
if (surface_data_->width <= 0 || surface_data_->height <= 0 || (surface_data_->data == nullptr)) {
|
||||
throw std::runtime_error("Invalid surface dimensions or data.");
|
||||
}
|
||||
|
||||
Uint32* pixels = nullptr;
|
||||
int pitch = 0;
|
||||
|
||||
SDL_Rect lock_rect;
|
||||
if (dest_rect != nullptr) {
|
||||
lock_rect.x = static_cast<int>(dest_rect->x);
|
||||
lock_rect.y = static_cast<int>(dest_rect->y);
|
||||
lock_rect.w = static_cast<int>(dest_rect->w);
|
||||
lock_rect.h = static_cast<int>(dest_rect->h);
|
||||
}
|
||||
|
||||
// Usa lockRect solo si destRect no es nulo
|
||||
if (!SDL_LockTexture(texture, (dest_rect != nullptr) ? &lock_rect : nullptr, reinterpret_cast<void**>(&pixels), &pitch)) {
|
||||
throw std::runtime_error("Failed to lock texture: " + std::string(SDL_GetError()));
|
||||
}
|
||||
|
||||
int row_stride = pitch / sizeof(Uint32);
|
||||
|
||||
// Cachear punteros fuera del bucle para permitir autovectorización SIMD
|
||||
const Uint8* src = surface_data_->data.get();
|
||||
const Uint32* pal = palette_.data();
|
||||
const int WIDTH = surface_data_->width;
|
||||
const int HEIGHT = surface_data_->height;
|
||||
for (int y = 0; y < HEIGHT; ++y) {
|
||||
const Uint8* src_row = src + (y * WIDTH);
|
||||
Uint32* dst_row = pixels + (y * row_stride);
|
||||
for (int x = 0; x < WIDTH; ++x) {
|
||||
dst_row[x] = pal[src_row[x]];
|
||||
}
|
||||
}
|
||||
|
||||
SDL_UnlockTexture(texture);
|
||||
|
||||
// Renderiza la textura con los rectángulos especificados
|
||||
if (!SDL_RenderTexture(renderer, texture, src_rect, dest_rect)) {
|
||||
throw std::runtime_error("Failed to copy texture to renderer: " + std::string(SDL_GetError()));
|
||||
}
|
||||
}
|
||||
|
||||
// Realiza un efecto de fundido en la paleta principal
|
||||
auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Verificar que el tamaño mínimo de palette_ sea adecuado
|
||||
static constexpr int PALETTE_SIZE = 19;
|
||||
if (sizeof(palette_) / sizeof(palette_[0]) < PALETTE_SIZE) {
|
||||
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
|
||||
}
|
||||
|
||||
// Desplazar colores (pares e impares)
|
||||
for (int i = 18; i > 1; --i) {
|
||||
palette_[i] = palette_[i - 2];
|
||||
}
|
||||
|
||||
// Ajustar el primer color
|
||||
palette_[1] = palette_[0];
|
||||
|
||||
// Devolver si el índice 15 coincide con el índice 0
|
||||
return palette_[15] == palette_[0];
|
||||
}
|
||||
|
||||
// Realiza un efecto de fundido en la paleta secundaria
|
||||
auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Variable estática para almacenar el último tick
|
||||
static Uint32 last_tick_ = 0;
|
||||
|
||||
// Obtener el tiempo actual
|
||||
Uint32 current_tick = SDL_GetTicks();
|
||||
|
||||
// Verificar si ha pasado el tiempo de retardo
|
||||
if (current_tick - last_tick_ < delay) {
|
||||
return false; // No se realiza el fade
|
||||
}
|
||||
|
||||
// Actualizar el último tick
|
||||
last_tick_ = current_tick;
|
||||
|
||||
// Verificar que el tamaño mínimo de sub_palette_ sea adecuado
|
||||
static constexpr int SUB_PALETTE_SIZE = 19;
|
||||
if (sizeof(sub_palette_) / sizeof(sub_palette_[0]) < SUB_PALETTE_SIZE) {
|
||||
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
|
||||
}
|
||||
|
||||
// Desplazar colores (pares e impares)
|
||||
for (int i = 18; i > 1; --i) {
|
||||
sub_palette_[i] = sub_palette_[i - 2];
|
||||
}
|
||||
|
||||
// Ajustar el primer color
|
||||
sub_palette_[1] = sub_palette_[0];
|
||||
|
||||
// Devolver si el índice 15 coincide con el índice 0
|
||||
return sub_palette_[15] == sub_palette_[0];
|
||||
}
|
||||
|
||||
// Restaura la sub paleta a su estado original
|
||||
void Surface::resetSubPalette() { initializeSubPalette(sub_palette_); } // NOLINT(readability-convert-member-functions-to-static)
|
||||
153
source/core/rendering/surface.hpp
Normal file
153
source/core/rendering/surface.hpp
Normal file
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array> // Para array
|
||||
#include <memory> // Para default_delete, shared_ptr, __shared_pt...
|
||||
#include <numeric> // Para iota
|
||||
#include <string> // Para string
|
||||
#include <utility> // Para move
|
||||
|
||||
#include "utils/utils.hpp" // Para PaletteColor
|
||||
|
||||
// Alias
|
||||
using Palette = std::array<Uint32, 256>;
|
||||
using SubPalette = std::array<Uint8, 256>;
|
||||
|
||||
// Carga una paleta desde un archivo .gif
|
||||
auto loadPalette(const std::string& file_path) -> Palette;
|
||||
|
||||
// Carga una paleta desde un archivo .pal
|
||||
auto readPalFile(const std::string& file_path) -> Palette;
|
||||
|
||||
struct SurfaceData {
|
||||
std::shared_ptr<Uint8[]> data; // Usa std::shared_ptr para gestión automática
|
||||
float width; // Ancho de la imagen
|
||||
float height; // Alto de la imagen
|
||||
|
||||
// Constructor por defecto
|
||||
SurfaceData()
|
||||
: data(nullptr),
|
||||
width(0),
|
||||
height(0) {}
|
||||
|
||||
// Constructor que inicializa dimensiones y asigna memoria
|
||||
SurfaceData(float w, float h)
|
||||
: data(std::shared_ptr<Uint8[]>(new Uint8[static_cast<size_t>(w * h)](), std::default_delete<Uint8[]>())),
|
||||
width(w),
|
||||
height(h) {}
|
||||
|
||||
// Constructor para inicializar directamente con datos
|
||||
SurfaceData(float w, float h, std::shared_ptr<Uint8[]> pixels)
|
||||
: data(std::move(pixels)),
|
||||
width(w),
|
||||
height(h) {}
|
||||
|
||||
// Constructor de movimiento
|
||||
SurfaceData(SurfaceData&& other) noexcept = default;
|
||||
|
||||
// Operador de movimiento
|
||||
auto operator=(SurfaceData&& other) noexcept -> SurfaceData& = default;
|
||||
|
||||
// Evita copias accidentales
|
||||
SurfaceData(const SurfaceData&) = delete;
|
||||
auto operator=(const SurfaceData&) -> SurfaceData& = delete;
|
||||
};
|
||||
|
||||
class Surface {
|
||||
private:
|
||||
std::shared_ptr<SurfaceData> surface_data_; // Datos a dibujar
|
||||
Palette palette_; // Paleta para volcar la SurfaceData a una Textura
|
||||
SubPalette sub_palette_; // Paleta para reindexar colores
|
||||
int transparent_color_; // Indice de la paleta que se omite en la copia de datos
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
Surface(int w, int h);
|
||||
explicit Surface(const std::string& file_path);
|
||||
|
||||
// Destructor
|
||||
~Surface() = default;
|
||||
|
||||
// Carga una SurfaceData desde un archivo
|
||||
static auto loadSurface(const std::string& file_path) -> SurfaceData;
|
||||
|
||||
// Carga una paleta desde un archivo
|
||||
void loadPalette(const std::string& file_path);
|
||||
void loadPalette(const Palette& palette);
|
||||
|
||||
// Copia una región de la SurfaceData de origen a la SurfaceData de destino
|
||||
void render(float dx, float dy, float sx, float sy, float w, float h);
|
||||
void render(int x, int y, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
|
||||
void render(SDL_FRect* src_rect = nullptr, SDL_FRect* dst_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
|
||||
|
||||
// Copia una región de la SurfaceData de origen a la SurfaceData de destino reemplazando un color por otro
|
||||
void renderWithColorReplace(int x, int y, Uint8 source_color = 0, Uint8 target_color = 0, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE) const;
|
||||
|
||||
// Render amb dissolució als cantons superior/inferior (hash 2D, sense parpelleig)
|
||||
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect = nullptr) const;
|
||||
|
||||
// Idem però reemplaçant un color índex (per a sprites sobre fons del mateix color)
|
||||
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect = nullptr) const;
|
||||
|
||||
// Establece un color en la paleta
|
||||
void setColor(int index, Uint32 color);
|
||||
|
||||
// Rellena la SurfaceData con un color
|
||||
void clear(Uint8 color);
|
||||
|
||||
// Vuelca la SurfaceData a una textura
|
||||
void copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture);
|
||||
void copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect);
|
||||
|
||||
// Realiza un efecto de fundido en las paletas
|
||||
auto fadePalette() -> bool;
|
||||
auto fadeSubPalette(Uint32 delay = 0) -> bool;
|
||||
|
||||
// Restaura la sub paleta a su estado original
|
||||
void resetSubPalette();
|
||||
|
||||
// Vuelca los píxeles como ARGB8888 a un buffer externo (sin SDL_Texture)
|
||||
void toARGBBuffer(Uint32* buffer) const;
|
||||
|
||||
// Pone un pixel en la SurfaceData
|
||||
void putPixel(int x, int y, Uint8 color);
|
||||
|
||||
// Obtiene el color de un pixel de la surface_data
|
||||
auto getPixel(int x, int y) -> Uint8;
|
||||
|
||||
// Dibuja un rectangulo relleno
|
||||
void fillRect(const SDL_FRect* rect, Uint8 color);
|
||||
|
||||
// Dibuja el borde de un rectangulo
|
||||
void drawRectBorder(const SDL_FRect* rect, Uint8 color);
|
||||
|
||||
// Dibuja una linea
|
||||
void drawLine(float x1, float y1, float x2, float y2, Uint8 color);
|
||||
|
||||
// Metodos para gestionar surface_data_
|
||||
[[nodiscard]] auto getSurfaceData() const -> std::shared_ptr<SurfaceData> { return surface_data_; }
|
||||
void setSurfaceData(std::shared_ptr<SurfaceData> new_data) { surface_data_ = std::move(new_data); }
|
||||
|
||||
// Obtien ancho y alto
|
||||
[[nodiscard]] auto getWidth() const -> float { return surface_data_->width; }
|
||||
[[nodiscard]] auto getHeight() const -> float { return surface_data_->height; }
|
||||
|
||||
// Color transparente
|
||||
[[nodiscard]] auto getTransparentColor() const -> Uint8 { return transparent_color_; }
|
||||
void setTransparentColor(Uint8 color = 255) { transparent_color_ = color; }
|
||||
|
||||
// Paleta
|
||||
void setPalette(const std::array<Uint32, 256>& palette) { palette_ = palette; }
|
||||
[[nodiscard]] auto getPaletteColor(Uint8 index) const -> Uint32 { return palette_[index]; }
|
||||
|
||||
// Inicializa la sub paleta
|
||||
static void initializeSubPalette(SubPalette& palette) { std::iota(palette.begin(), palette.end(), 0); }
|
||||
|
||||
private:
|
||||
// Helper para calcular coordenadas con flip
|
||||
static void calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y);
|
||||
|
||||
// Helper para copiar un pixel si no es transparente
|
||||
void copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const;
|
||||
};
|
||||
311
source/core/rendering/text.cpp
Normal file
311
source/core/rendering/text.cpp
Normal file
@@ -0,0 +1,311 @@
|
||||
#include "core/rendering/text.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef> // Para size_t
|
||||
#include <iostream> // Para cerr
|
||||
#include <sstream> // Para istringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "utils/utils.hpp" // Para getFileName, stringToColor, printWithDots
|
||||
|
||||
// Extrae el siguiente codepoint UTF-8 de la cadena, avanzando 'pos' al byte siguiente
|
||||
auto Text::nextCodepoint(const std::string& s, size_t& pos) -> uint32_t { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto c = static_cast<unsigned char>(s[pos]);
|
||||
uint32_t cp = 0;
|
||||
size_t extra = 0;
|
||||
|
||||
if (c < 0x80) {
|
||||
cp = c;
|
||||
extra = 0;
|
||||
} else if (c < 0xC0) {
|
||||
pos++;
|
||||
return 0xFFFD;
|
||||
} // byte de continuación suelto
|
||||
else if (c < 0xE0) {
|
||||
cp = c & 0x1F;
|
||||
extra = 1;
|
||||
} else if (c < 0xF0) {
|
||||
cp = c & 0x0F;
|
||||
extra = 2;
|
||||
} else if (c < 0xF8) {
|
||||
cp = c & 0x07;
|
||||
extra = 3;
|
||||
} else {
|
||||
pos++;
|
||||
return 0xFFFD;
|
||||
}
|
||||
|
||||
pos++;
|
||||
for (size_t i = 0; i < extra && pos < s.size(); ++i, ++pos) {
|
||||
auto cb = static_cast<unsigned char>(s[pos]);
|
||||
if ((cb & 0xC0) != 0x80) { return 0xFFFD; }
|
||||
cp = (cp << 6) | (cb & 0x3F);
|
||||
}
|
||||
return cp;
|
||||
}
|
||||
|
||||
// Convierte un codepoint Unicode a una cadena UTF-8
|
||||
auto Text::codepointToUtf8(uint32_t cp) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::string result;
|
||||
if (cp < 0x80) {
|
||||
result += static_cast<char>(cp);
|
||||
} else if (cp < 0x800) {
|
||||
result += static_cast<char>(0xC0 | (cp >> 6));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else if (cp < 0x10000) {
|
||||
result += static_cast<char>(0xE0 | (cp >> 12));
|
||||
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else {
|
||||
result += static_cast<char>(0xF0 | (cp >> 18));
|
||||
result += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
result += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Carga un fichero de definición de fuente .fnt
|
||||
// Formato: líneas "clave valor", comentarios con #, gliphos como "codepoint ancho"
|
||||
auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto tf = std::make_shared<File>();
|
||||
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
if (file_data.empty()) {
|
||||
std::cerr << "Error: Fichero no encontrado " << getFileName(file_path) << '\n';
|
||||
throw std::runtime_error("Fichero no encontrado: " + getFileName(file_path));
|
||||
}
|
||||
|
||||
std::string content(file_data.begin(), file_data.end());
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
int glyph_index = 0;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
||||
if (line.empty() || line[0] == '#') { continue; }
|
||||
|
||||
std::istringstream ls(line);
|
||||
std::string key;
|
||||
ls >> key;
|
||||
|
||||
if (key == "box_width") {
|
||||
ls >> tf->box_width;
|
||||
} else if (key == "box_height") {
|
||||
ls >> tf->box_height;
|
||||
} else if (key == "columns") {
|
||||
ls >> tf->columns;
|
||||
} else if (key == "cell_spacing") {
|
||||
ls >> tf->cell_spacing;
|
||||
} else if (key == "row_spacing") {
|
||||
ls >> tf->row_spacing;
|
||||
} else {
|
||||
// Línea de glifo: codepoint_decimal ancho_visual
|
||||
uint32_t codepoint = 0;
|
||||
int width = 0;
|
||||
try {
|
||||
codepoint = static_cast<uint32_t>(std::stoul(key));
|
||||
ls >> width;
|
||||
} catch (...) {
|
||||
continue; // línea mal formateada, ignorar
|
||||
}
|
||||
Offset off{};
|
||||
const int ROW_SP = tf->row_spacing > 0 ? tf->row_spacing : tf->cell_spacing;
|
||||
off.x = ((glyph_index % tf->columns) * (tf->box_width + tf->cell_spacing)) + tf->cell_spacing;
|
||||
off.y = ((glyph_index / tf->columns) * (tf->box_height + ROW_SP)) + tf->cell_spacing;
|
||||
off.w = width;
|
||||
tf->offset[codepoint] = off;
|
||||
++glyph_index;
|
||||
}
|
||||
}
|
||||
|
||||
printWithDots("Text File : ", getFileName(file_path), "[ LOADED ]");
|
||||
return tf;
|
||||
}
|
||||
|
||||
// Constructor desde fichero
|
||||
Text::Text(const std::shared_ptr<Surface>& surface, const std::string& text_file) {
|
||||
auto tf = loadTextFile(text_file);
|
||||
|
||||
box_height_ = tf->box_height;
|
||||
box_width_ = tf->box_width;
|
||||
offset_ = tf->offset;
|
||||
|
||||
sprite_ = std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
|
||||
}
|
||||
|
||||
// Constructor desde estructura precargada
|
||||
Text::Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file)
|
||||
: sprite_(std::make_unique<Sprite>(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(text_file->box_width), .h = static_cast<float>(text_file->box_height)})),
|
||||
box_width_(text_file->box_width),
|
||||
box_height_(text_file->box_height),
|
||||
offset_(text_file->offset) {
|
||||
}
|
||||
|
||||
// Escribe texto en pantalla
|
||||
void Text::write(int x, int y, const std::string& text, int kerning, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int shift = 0;
|
||||
int glyphs_done = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
sprite_->setY(y);
|
||||
while (pos < text.size()) {
|
||||
if (lenght != -1 && glyphs_done >= lenght) { break; }
|
||||
uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
sprite_->setClip(it->second.x, it->second.y, box_width_, box_height_);
|
||||
sprite_->setX(x + shift);
|
||||
sprite_->render(1, 15);
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
++glyphs_done;
|
||||
}
|
||||
}
|
||||
|
||||
// Escribe el texto en una surface
|
||||
auto Text::writeToSurface(const std::string& text, int zoom, int kerning) -> std::shared_ptr<Surface> { // NOLINT(readability-make-member-function-const)
|
||||
auto width = length(text, kerning) * zoom;
|
||||
auto height = box_height_ * zoom;
|
||||
auto surface = std::make_shared<Surface>(width, height);
|
||||
auto previuos_renderer = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(surface);
|
||||
surface->clear(stringToColor("transparent"));
|
||||
write(0, 0, text, kerning);
|
||||
Screen::get()->setRendererSurface(previuos_renderer);
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
// Escribe el texto con extras en una surface
|
||||
auto Text::writeDXToSurface(Uint8 flags, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) -> std::shared_ptr<Surface> { // NOLINT(readability-make-member-function-const)
|
||||
auto width = Text::length(text, kerning) + shadow_distance;
|
||||
auto height = box_height_ + shadow_distance;
|
||||
auto surface = std::make_shared<Surface>(width, height);
|
||||
auto previuos_renderer = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(surface);
|
||||
surface->clear(stringToColor("transparent"));
|
||||
writeDX(flags, 0, 0, text, kerning, text_color, shadow_distance, shadow_color, lenght);
|
||||
Screen::get()->setRendererSurface(previuos_renderer);
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
// Escribe el texto con colores
|
||||
void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int kerning, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int shift = 0;
|
||||
int glyphs_done = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
sprite_->setY(y);
|
||||
while (pos < text.size()) {
|
||||
if (lenght != -1 && glyphs_done >= lenght) { break; }
|
||||
uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
sprite_->setClip(it->second.x, it->second.y, box_width_, box_height_);
|
||||
sprite_->setX(x + shift);
|
||||
sprite_->render(1, color);
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
++glyphs_done;
|
||||
}
|
||||
}
|
||||
|
||||
// Escribe el texto con sombra
|
||||
void Text::writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance, int kerning, int lenght) {
|
||||
writeColored(x + shadow_distance, y + shadow_distance, text, color, kerning, lenght);
|
||||
write(x, y, text, kerning, lenght);
|
||||
}
|
||||
|
||||
// Escribe el texto centrado en un punto x
|
||||
void Text::writeCentered(int x, int y, const std::string& text, int kerning, int lenght) {
|
||||
x -= (Text::length(text, kerning) / 2);
|
||||
write(x, y, text, kerning, lenght);
|
||||
}
|
||||
|
||||
// Escribe texto con extras
|
||||
void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const auto CENTERED = ((flags & CENTER_FLAG) == CENTER_FLAG);
|
||||
const auto SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG);
|
||||
const auto COLORED = ((flags & COLOR_FLAG) == COLOR_FLAG);
|
||||
const auto STROKED = ((flags & STROKE_FLAG) == STROKE_FLAG);
|
||||
|
||||
if (CENTERED) {
|
||||
x -= (Text::length(text, kerning) / 2);
|
||||
}
|
||||
|
||||
if (SHADOWED) {
|
||||
writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, lenght);
|
||||
}
|
||||
|
||||
if (STROKED) {
|
||||
const int MAX_DIST = static_cast<int>(shadow_distance);
|
||||
for (int dist = 1; dist <= MAX_DIST; ++dist) {
|
||||
for (int dy = -dist; dy <= dist; ++dy) {
|
||||
for (int dx = -dist; dx <= dist; ++dx) {
|
||||
writeColored(x + dx, y + dy, text, shadow_color, kerning, lenght);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (COLORED) {
|
||||
writeColored(x, y, text, text_color, kerning, lenght);
|
||||
} else {
|
||||
writeColored(x, y, text, text_color, kerning, lenght);
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene la longitud en pixels de una cadena UTF-8
|
||||
auto Text::length(const std::string& text, int kerning) const -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int shift = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < text.size()) {
|
||||
uint32_t cp = nextCodepoint(text, pos);
|
||||
auto it = offset_.find(cp);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) {
|
||||
shift += it->second.w + kerning;
|
||||
}
|
||||
}
|
||||
|
||||
return shift > 0 ? shift - kerning : 0;
|
||||
}
|
||||
|
||||
// Devuelve el ancho en pixels de un glifo dado su codepoint Unicode
|
||||
auto Text::glyphWidth(uint32_t codepoint, int kerning) const -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = offset_.find(codepoint);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it != offset_.end()) { return it->second.w + kerning; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Devuelve el clip rect (región en el bitmap) de un glifo dado su codepoint
|
||||
auto Text::getGlyphClip(uint32_t codepoint) const -> SDL_FRect {
|
||||
auto it = offset_.find(codepoint);
|
||||
if (it == offset_.end()) { it = offset_.find('?'); }
|
||||
if (it == offset_.end()) { return {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; }
|
||||
return {.x = static_cast<float>(it->second.x),
|
||||
.y = static_cast<float>(it->second.y),
|
||||
.w = static_cast<float>(box_width_),
|
||||
.h = static_cast<float>(box_height_)};
|
||||
}
|
||||
|
||||
// Devuelve el tamaño de la caja de cada caracter
|
||||
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;
|
||||
}
|
||||
72
source/core/rendering/text.hpp
Normal file
72
source/core/rendering/text.hpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr, unique_ptr
|
||||
#include <string> // Para string
|
||||
#include <unordered_map> // Para unordered_map
|
||||
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
class Surface; // Forward declaration
|
||||
|
||||
// Clase texto. Pinta texto en pantalla a partir de un bitmap con soporte UTF-8
|
||||
class Text {
|
||||
public:
|
||||
// Tipos anidados públicos
|
||||
struct Offset {
|
||||
int x{0}, y{0}, w{0};
|
||||
};
|
||||
|
||||
struct File {
|
||||
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
|
||||
int columns{16}; // Número de columnas en el bitmap
|
||||
int cell_spacing{0}; // Píxeles de separación entre columnas (y borde izquierdo/superior)
|
||||
int row_spacing{0}; // Píxeles de separación entre filas (si difiere de cell_spacing)
|
||||
std::unordered_map<uint32_t, Offset> offset; // Posición y ancho de cada glifo (clave: codepoint Unicode)
|
||||
};
|
||||
|
||||
// Constructor
|
||||
Text(const std::shared_ptr<Surface>& surface, const std::string& text_file);
|
||||
Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file);
|
||||
|
||||
// Destructor
|
||||
~Text() = default;
|
||||
|
||||
// Constantes de flags para writeDX
|
||||
static constexpr int COLOR_FLAG = 1;
|
||||
static constexpr int SHADOW_FLAG = 2;
|
||||
static constexpr int CENTER_FLAG = 4;
|
||||
static constexpr int STROKE_FLAG = 8;
|
||||
|
||||
void write(int x, int y, const std::string& text, int kerning = 1, int lenght = -1); // Escribe el texto en pantalla
|
||||
void writeColored(int x, int y, const std::string& text, Uint8 color, int kerning = 1, int lenght = -1); // Escribe el texto con colores
|
||||
void writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance = 1, int kerning = 1, int lenght = -1); // Escribe el texto con sombra
|
||||
void writeCentered(int x, int y, const std::string& text, int kerning = 1, int lenght = -1); // Escribe el texto centrado en un punto x
|
||||
void writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning = 1, Uint8 text_color = Uint8(), Uint8 shadow_distance = 1, Uint8 shadow_color = Uint8(), int lenght = -1); // Escribe texto con extras
|
||||
|
||||
auto writeToSurface(const std::string& text, int zoom = 1, int kerning = 1) -> std::shared_ptr<Surface>; // Escribe el texto en una textura
|
||||
auto writeDXToSurface(Uint8 flags, const std::string& text, int kerning = 1, Uint8 text_color = Uint8(), Uint8 shadow_distance = 1, Uint8 shadow_color = Uint8(), int lenght = -1) -> std::shared_ptr<Surface>; // Escribe el texto con extras en una textura
|
||||
|
||||
[[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 del caracter
|
||||
[[nodiscard]] auto glyphWidth(uint32_t codepoint, int kerning = 0) const -> int; // Devuelve el ancho en pixels de un glifo
|
||||
[[nodiscard]] auto getGlyphClip(uint32_t codepoint) const -> SDL_FRect; // Devuelve el clip rect del glifo
|
||||
[[nodiscard]] auto getSprite() const -> Sprite* { return sprite_.get(); } // Acceso al sprite interno
|
||||
|
||||
void setFixedWidth(bool value); // Establece si se usa un tamaño fijo de letra
|
||||
|
||||
static auto loadTextFile(const std::string& file_path) -> std::shared_ptr<File>; // Carga un fichero de definición de fuente .fnt
|
||||
static auto codepointToUtf8(uint32_t cp) -> std::string; // Convierte un codepoint Unicode a string UTF-8
|
||||
static auto nextCodepoint(const std::string& s, size_t& pos) -> uint32_t; // Extrae el siguiente codepoint UTF-8
|
||||
|
||||
private:
|
||||
// Objetos y punteros
|
||||
std::unique_ptr<Sprite> sprite_ = nullptr; // Objeto con los graficos para el texto
|
||||
|
||||
// Variables
|
||||
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
|
||||
std::unordered_map<uint32_t, Offset> offset_; // Posición y ancho de cada glifo (clave: codepoint Unicode)
|
||||
};
|
||||
533
source/core/resources/resource_cache.cpp
Normal file
533
source/core/resources/resource_cache.cpp
Normal file
@@ -0,0 +1,533 @@
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para find_if
|
||||
#include <cstdlib> // Para exit, size_t
|
||||
#include <fstream> // Para ifstream, istreambuf_iterator
|
||||
#include <iostream> // Para basic_ostream, operator<<, endl, cout
|
||||
#include <stdexcept> // Para runtime_error
|
||||
#include <utility>
|
||||
|
||||
#include "core/audio/jail_audio.hpp" // Para JA_DeleteMusic, JA_DeleteSound, JA_Loa...
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/text.hpp" // Para Text, loadTextFile
|
||||
#include "core/resources/resource_helper.hpp" // Para Helper
|
||||
#include "core/resources/resource_list.hpp" // Para List, List::Type
|
||||
#include "game/defaults.hpp" // Para Defaults namespace
|
||||
#include "game/gameplay/room.hpp" // Para RoomData, loadRoomFile, loadRoomTileFile
|
||||
#include "game/gameplay/room_loader.hpp" // Para RoomLoader::loadFromString
|
||||
#include "game/options.hpp" // Para Options, OptionsGame, options
|
||||
#include "utils/defines.hpp" // Para WINDOW_CAPTION
|
||||
#include "utils/utils.hpp" // Para getFileName, printWithDots, PaletteColor
|
||||
#include "version.h" // Para Version::GIT_HASH
|
||||
struct JA_Music_t; // lines 17-17
|
||||
struct JA_Sound_t; // lines 18-18
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// [SINGLETON] Hay que definir las variables estáticas, desde el .h sólo la hemos declarado
|
||||
Cache* Cache::cache = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto cache con esta función estática
|
||||
void Cache::init() { Cache::cache = new Cache(); }
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto cache con esta función estática
|
||||
void Cache::destroy() { delete Cache::cache; }
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto cache y podemos trabajar con él
|
||||
auto Cache::get() -> Cache* { return Cache::cache; }
|
||||
|
||||
// Constructor
|
||||
Cache::Cache()
|
||||
: loading_text_(Screen::get()->getText()) {
|
||||
load();
|
||||
}
|
||||
|
||||
// Vacia todos los vectores de recursos
|
||||
void Cache::clear() {
|
||||
clearSounds();
|
||||
clearMusics();
|
||||
surfaces_.clear();
|
||||
palettes_.clear();
|
||||
text_files_.clear();
|
||||
texts_.clear();
|
||||
animations_.clear();
|
||||
}
|
||||
|
||||
// Carga todos los recursos
|
||||
void Cache::load() {
|
||||
// Nota: el overlay de debug (RenderInfo) se inicializa después de esta carga,
|
||||
// por lo que updateZoomFactor() se llamará correctamente en RenderInfo::init().
|
||||
calculateTotal();
|
||||
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK));
|
||||
std::cout << "\n** LOADING RESOURCES" << '\n';
|
||||
loadSounds();
|
||||
loadMusics();
|
||||
loadSurfaces();
|
||||
loadPalettes();
|
||||
loadTextFiles();
|
||||
loadAnimations();
|
||||
loadRooms();
|
||||
createText();
|
||||
std::cout << "\n** RESOURCES LOADED" << '\n';
|
||||
}
|
||||
|
||||
// Recarga todos los recursos
|
||||
void Cache::reload() {
|
||||
clear();
|
||||
load();
|
||||
}
|
||||
|
||||
// Obtiene el sonido a partir de un nombre
|
||||
auto Cache::getSound(const std::string& name) -> JA_Sound_t* { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(sounds_, [&name](const auto& s) -> bool { return s.name == name; });
|
||||
|
||||
if (it != sounds_.end()) {
|
||||
return it->sound;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Sonido no encontrado " << name << '\n';
|
||||
throw std::runtime_error("Sonido no encontrado: " + name);
|
||||
}
|
||||
|
||||
// Obtiene la música a partir de un nombre
|
||||
auto Cache::getMusic(const std::string& name) -> JA_Music_t* { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(musics_, [&name](const auto& m) -> bool { return m.name == name; });
|
||||
|
||||
if (it != musics_.end()) {
|
||||
return it->music;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Música no encontrada " << name << '\n';
|
||||
throw std::runtime_error("Música no encontrada: " + name);
|
||||
}
|
||||
|
||||
// Obtiene la surface a partir de un nombre
|
||||
auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(surfaces_, [&name](const auto& t) -> bool { return t.name == name; });
|
||||
|
||||
if (it != surfaces_.end()) {
|
||||
return it->surface;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Imagen no encontrada " << name << '\n';
|
||||
throw std::runtime_error("Imagen no encontrada: " + name);
|
||||
}
|
||||
|
||||
// Obtiene la paleta a partir de un nombre
|
||||
auto Cache::getPalette(const std::string& name) -> Palette { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(palettes_, [&name](const auto& t) -> bool { return t.name == name; });
|
||||
|
||||
if (it != palettes_.end()) {
|
||||
return it->palette;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Paleta no encontrada " << name << '\n';
|
||||
throw std::runtime_error("Paleta no encontrada: " + name);
|
||||
}
|
||||
|
||||
// Obtiene el fichero de texto a partir de un nombre
|
||||
auto Cache::getTextFile(const std::string& name) -> std::shared_ptr<Text::File> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(text_files_, [&name](const auto& t) -> bool { return t.name == name; });
|
||||
|
||||
if (it != text_files_.end()) {
|
||||
return it->text_file;
|
||||
}
|
||||
|
||||
std::cerr << "Error: TextFile no encontrado " << name << '\n';
|
||||
throw std::runtime_error("TextFile no encontrado: " + name);
|
||||
}
|
||||
|
||||
// Obtiene el objeto de texto a partir de un nombre
|
||||
auto Cache::getText(const std::string& name) -> std::shared_ptr<Text> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(texts_, [&name](const auto& t) -> bool { return t.name == name; });
|
||||
|
||||
if (it != texts_.end()) {
|
||||
return it->text;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Text no encontrado " << name << '\n';
|
||||
throw std::runtime_error("Texto no encontrado: " + name);
|
||||
}
|
||||
|
||||
// Obtiene los datos de animación parseados a partir de un nombre
|
||||
auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(animations_, [&name](const auto& a) -> bool { return a.name == name; });
|
||||
|
||||
if (it != animations_.end()) {
|
||||
return *it;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Animación no encontrada " << name << '\n';
|
||||
throw std::runtime_error("Animación no encontrada: " + name);
|
||||
}
|
||||
|
||||
// Obtiene la habitación a partir de un nombre
|
||||
auto Cache::getRoom(const std::string& name) -> std::shared_ptr<Room::Data> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = std::ranges::find_if(rooms_, [&name](const auto& r) -> bool { return r.name == name; });
|
||||
|
||||
if (it != rooms_.end()) {
|
||||
return it->room;
|
||||
}
|
||||
|
||||
std::cerr << "Error: Habitación no encontrada " << name << '\n';
|
||||
throw std::runtime_error("Habitación no encontrada: " + name);
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Recarga una habitación desde disco (para el editor de mapas)
|
||||
// Lee directamente del filesystem (no del resource pack) para obtener los cambios del editor
|
||||
void Cache::reloadRoom(const std::string& name) {
|
||||
auto file_path = List::get()->get(name);
|
||||
if (file_path.empty()) {
|
||||
std::cerr << "reloadRoom: Cannot resolve path for " << name << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// Leer directamente del filesystem (evita el resource pack que tiene datos antiguos)
|
||||
std::ifstream file(file_path);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "reloadRoom: Cannot open " << file_path << '\n';
|
||||
return;
|
||||
}
|
||||
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
// Parsear y actualizar el cache
|
||||
auto it = std::ranges::find_if(rooms_, [&name](const auto& r) -> bool { return r.name == name; });
|
||||
if (it != rooms_.end()) {
|
||||
*(it->room) = RoomLoader::loadFromString(content, name);
|
||||
std::cout << "reloadRoom: " << name << " reloaded from filesystem\n";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Obtiene todas las habitaciones
|
||||
auto Cache::getRooms() -> std::vector<RoomResource>& {
|
||||
return rooms_;
|
||||
}
|
||||
|
||||
// Helper para lanzar errores de carga con formato consistente
|
||||
[[noreturn]] void Cache::throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cerr << "\n[ ERROR ] Failed to load " << asset_type << ": " << getFileName(file_path) << '\n';
|
||||
std::cerr << "[ ERROR ] Path: " << file_path << '\n';
|
||||
std::cerr << "[ ERROR ] Reason: " << e.what() << '\n';
|
||||
std::cerr << "[ ERROR ] Check config/assets.yaml configuration\n";
|
||||
throw;
|
||||
}
|
||||
|
||||
// Carga los sonidos
|
||||
void Cache::loadSounds() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> SOUND FILES" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::SOUND);
|
||||
sounds_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
JA_Sound_t* sound = nullptr;
|
||||
|
||||
// Try loading from resource pack first
|
||||
auto audio_data = Helper::loadFile(l);
|
||||
if (!audio_data.empty()) {
|
||||
sound = JA_LoadSound(audio_data.data(), static_cast<Uint32>(audio_data.size()));
|
||||
}
|
||||
|
||||
// Fallback to file path if memory loading failed
|
||||
if (sound == nullptr) {
|
||||
sound = JA_LoadSound(l.c_str());
|
||||
}
|
||||
|
||||
if (sound == nullptr) {
|
||||
throw std::runtime_error("Failed to decode audio file");
|
||||
}
|
||||
|
||||
sounds_.emplace_back(SoundResource{.name = name, .sound = sound});
|
||||
printWithDots("Sound : ", name, "[ LOADED ]");
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("SOUND", l, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga las musicas
|
||||
void Cache::loadMusics() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> MUSIC FILES" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::MUSIC);
|
||||
musics_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
JA_Music_t* music = nullptr;
|
||||
|
||||
// Try loading from resource pack first
|
||||
auto audio_data = Helper::loadFile(l);
|
||||
if (!audio_data.empty()) {
|
||||
music = JA_LoadMusic(audio_data.data(), static_cast<Uint32>(audio_data.size()));
|
||||
}
|
||||
|
||||
// Fallback to file path if memory loading failed
|
||||
if (music == nullptr) {
|
||||
music = JA_LoadMusic(l.c_str());
|
||||
}
|
||||
|
||||
if (music == nullptr) {
|
||||
throw std::runtime_error("Failed to decode music file");
|
||||
}
|
||||
|
||||
musics_.emplace_back(MusicResource{.name = name, .music = music});
|
||||
printWithDots("Music : ", name, "[ LOADED ]");
|
||||
updateLoadingProgress(1);
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("MUSIC", l, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga las texturas
|
||||
void Cache::loadSurfaces() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> SURFACES" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::BITMAP);
|
||||
surfaces_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared<Surface>(l)});
|
||||
surfaces_.back().surface->setTransparentColor(0);
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("BITMAP", l, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Reconfigura el color transparente de algunas surfaces
|
||||
getSurface("loading_screen_color.gif")->setTransparentColor();
|
||||
getSurface("ending1.gif")->setTransparentColor();
|
||||
getSurface("ending2.gif")->setTransparentColor();
|
||||
getSurface("ending3.gif")->setTransparentColor();
|
||||
getSurface("ending4.gif")->setTransparentColor();
|
||||
getSurface("ending5.gif")->setTransparentColor();
|
||||
getSurface("standard.gif")->setTransparentColor(16);
|
||||
}
|
||||
|
||||
// Carga las paletas
|
||||
void Cache::loadPalettes() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> PALETTES" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::PALETTE);
|
||||
palettes_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)});
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("PALETTE", l, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga los ficheros de texto
|
||||
void Cache::loadTextFiles() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> TEXT FILES" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::FONT);
|
||||
text_files_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)});
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("FONT", l, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga las animaciones
|
||||
void Cache::loadAnimations() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> ANIMATIONS" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::ANIMATION);
|
||||
animations_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
|
||||
// Cargar bytes del archivo YAML sin parsear (carga lazy)
|
||||
auto yaml_bytes = Helper::loadFile(l);
|
||||
|
||||
if (yaml_bytes.empty()) {
|
||||
throw std::runtime_error("File is empty or could not be loaded");
|
||||
}
|
||||
|
||||
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes});
|
||||
printWithDots("Animation : ", name, "[ LOADED ]");
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("ANIMATION", l, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga las habitaciones desde archivos YAML
|
||||
void Cache::loadRooms() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::cout << "\n>> ROOMS" << '\n';
|
||||
auto list = List::get()->getListByType(List::Type::ROOM);
|
||||
rooms_.clear();
|
||||
|
||||
for (const auto& l : list) {
|
||||
try {
|
||||
auto name = getFileName(l);
|
||||
rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared<Room::Data>(Room::loadYAML(l))});
|
||||
printWithDots("Room : ", name, "[ LOADED ]");
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("ROOM", l, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Cache::createText() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
struct ResourceInfo {
|
||||
std::string key; // Identificador del recurso
|
||||
std::string texture_file; // Nombre del archivo de textura
|
||||
std::string text_file; // Nombre del archivo de texto
|
||||
};
|
||||
|
||||
std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n';
|
||||
|
||||
std::vector<ResourceInfo> resources = {
|
||||
{.key = "aseprite", .texture_file = "aseprite.gif", .text_file = "aseprite.fnt"},
|
||||
{.key = "gauntlet", .texture_file = "gauntlet.gif", .text_file = "gauntlet.fnt"},
|
||||
{.key = "smb2", .texture_file = "smb2.gif", .text_file = "smb2.fnt"},
|
||||
{.key = "subatomic", .texture_file = "subatomic.gif", .text_file = "subatomic.fnt"},
|
||||
{.key = "8bithud", .texture_file = "8bithud.gif", .text_file = "8bithud.fnt"}};
|
||||
|
||||
for (const auto& res_info : resources) {
|
||||
texts_.emplace_back(TextResource{.name = res_info.key, .text = std::make_shared<Text>(getSurface(res_info.texture_file), getTextFile(res_info.text_file))});
|
||||
printWithDots("Text : ", res_info.key, "[ DONE ]");
|
||||
}
|
||||
}
|
||||
|
||||
// Vacía el vector de sonidos
|
||||
void Cache::clearSounds() {
|
||||
// Itera sobre el vector y libera los recursos asociados a cada JA_Sound_t
|
||||
for (auto& sound : sounds_) {
|
||||
if (sound.sound != nullptr) {
|
||||
JA_DeleteSound(sound.sound);
|
||||
sound.sound = nullptr;
|
||||
}
|
||||
}
|
||||
sounds_.clear(); // Limpia el vector después de liberar todos los recursos
|
||||
}
|
||||
|
||||
// Vacía el vector de musicas
|
||||
void Cache::clearMusics() {
|
||||
// Itera sobre el vector y libera los recursos asociados a cada JA_Music_t
|
||||
for (auto& music : musics_) {
|
||||
if (music.music != nullptr) {
|
||||
JA_DeleteMusic(music.music);
|
||||
music.music = nullptr;
|
||||
}
|
||||
}
|
||||
musics_.clear(); // Limpia el vector después de liberar todos los recursos
|
||||
}
|
||||
|
||||
// Calcula el numero de recursos para cargar
|
||||
void Cache::calculateTotal() {
|
||||
std::vector<List::Type> asset_types = {
|
||||
List::Type::SOUND,
|
||||
List::Type::MUSIC,
|
||||
List::Type::BITMAP,
|
||||
List::Type::PALETTE,
|
||||
List::Type::FONT,
|
||||
List::Type::ANIMATION,
|
||||
List::Type::ROOM};
|
||||
|
||||
int total = 0;
|
||||
for (const auto& asset_type : asset_types) {
|
||||
auto list = List::get()->getListByType(asset_type);
|
||||
total += list.size();
|
||||
}
|
||||
|
||||
count_ = ResourceCount{.total = total, .loaded = 0};
|
||||
}
|
||||
|
||||
// Muestra el progreso de carga
|
||||
void Cache::renderProgress() {
|
||||
constexpr float X_PADDING = 60.0F;
|
||||
constexpr float Y_PADDING = 10.0F;
|
||||
constexpr float BAR_HEIGHT = 5.0F;
|
||||
|
||||
const float BAR_POSITION = Options::game.height - BAR_HEIGHT - Y_PADDING;
|
||||
Screen::get()->start();
|
||||
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
|
||||
|
||||
auto surface = Screen::get()->getRendererSurface();
|
||||
const auto LOADING_TEXT_COLOR = static_cast<Uint8>(PaletteColor::BRIGHT_WHITE);
|
||||
const auto BAR_COLOR = static_cast<Uint8>(PaletteColor::WHITE);
|
||||
const int TEXT_HEIGHT = loading_text_->getCharacterSize();
|
||||
const int CENTER_X = Options::game.width / 2;
|
||||
const int CENTER_Y = Options::game.height / 2;
|
||||
|
||||
// Draw APP_NAME centered above center
|
||||
const std::string APP_NAME = spaceBetweenLetters(Version::APP_NAME);
|
||||
loading_text_->writeColored(
|
||||
CENTER_X - (loading_text_->length(APP_NAME) / 2),
|
||||
CENTER_Y - TEXT_HEIGHT,
|
||||
APP_NAME,
|
||||
LOADING_TEXT_COLOR);
|
||||
|
||||
// Draw VERSION centered below center
|
||||
const std::string VERSION_TEXT = "ver. " + std::string(Texts::VERSION) + " (" + std::string(Version::GIT_HASH) + ")";
|
||||
loading_text_->writeColored(
|
||||
CENTER_X - (loading_text_->length(VERSION_TEXT) / 2),
|
||||
CENTER_Y + TEXT_HEIGHT,
|
||||
VERSION_TEXT,
|
||||
LOADING_TEXT_COLOR);
|
||||
|
||||
// Draw progress bar border
|
||||
const float WIRED_BAR_WIDTH = Options::game.width - (X_PADDING * 2);
|
||||
SDL_FRect rect_wired = {.x = X_PADDING, .y = BAR_POSITION, .w = WIRED_BAR_WIDTH, .h = BAR_HEIGHT};
|
||||
surface->drawRectBorder(&rect_wired, BAR_COLOR);
|
||||
|
||||
// Draw progress bar fill
|
||||
const float FULL_BAR_WIDTH = WIRED_BAR_WIDTH * count_.getPercentage();
|
||||
SDL_FRect rect_full = {.x = X_PADDING, .y = BAR_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT};
|
||||
surface->fillRect(&rect_full, BAR_COLOR);
|
||||
|
||||
Screen::get()->render();
|
||||
}
|
||||
|
||||
// Comprueba los eventos de la pantalla de carga
|
||||
void Cache::checkEvents() {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_QUIT:
|
||||
exit(0);
|
||||
break;
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
if (event.key.key == SDLK_ESCAPE) {
|
||||
exit(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actualiza el progreso de carga
|
||||
void Cache::updateLoadingProgress(int steps) {
|
||||
count_.add(1);
|
||||
if (count_.loaded % steps == 0 || count_.loaded == count_.total) {
|
||||
renderProgress();
|
||||
}
|
||||
checkEvents();
|
||||
}
|
||||
|
||||
} // namespace Resource
|
||||
96
source/core/resources/resource_cache.hpp
Normal file
96
source/core/resources/resource_cache.hpp
Normal file
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
#include <utility>
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/resources/resource_types.hpp" // Para structs de recursos
|
||||
|
||||
namespace Resource {
|
||||
|
||||
class Cache {
|
||||
public:
|
||||
static void init(); // Inicialización singleton
|
||||
static void destroy(); // Destrucción singleton
|
||||
static auto get() -> Cache*; // Acceso al singleton
|
||||
|
||||
auto getSound(const std::string& name) -> JA_Sound_t*; // Getters de recursos
|
||||
auto getMusic(const std::string& name) -> JA_Music_t*;
|
||||
auto getSurface(const std::string& name) -> std::shared_ptr<Surface>;
|
||||
auto getPalette(const std::string& name) -> Palette;
|
||||
auto getTextFile(const std::string& name) -> std::shared_ptr<Text::File>;
|
||||
auto getText(const std::string& name) -> std::shared_ptr<Text>;
|
||||
auto getAnimationData(const std::string& name) -> const AnimationResource&;
|
||||
auto getRoom(const std::string& name) -> std::shared_ptr<Room::Data>;
|
||||
auto getRooms() -> std::vector<RoomResource>&;
|
||||
|
||||
void reload(); // Recarga todos los recursos
|
||||
#ifdef _DEBUG
|
||||
void reloadRoom(const std::string& name); // Recarga una habitación desde disco
|
||||
#endif
|
||||
|
||||
private:
|
||||
// Estructura para llevar la cuenta de los recursos cargados
|
||||
struct ResourceCount {
|
||||
int total{0}; // Número total de recursos
|
||||
int loaded{0}; // Número de recursos cargados
|
||||
|
||||
// Añade una cantidad a los recursos cargados
|
||||
void add(int amount) {
|
||||
loaded += amount;
|
||||
}
|
||||
|
||||
// Obtiene el porcentaje de recursos cargados
|
||||
[[nodiscard]] auto getPercentage() const -> float {
|
||||
return static_cast<float>(loaded) / static_cast<float>(total);
|
||||
}
|
||||
};
|
||||
|
||||
// Métodos de carga de recursos
|
||||
void loadSounds();
|
||||
void loadMusics();
|
||||
void loadSurfaces();
|
||||
void loadPalettes();
|
||||
void loadTextFiles();
|
||||
void loadAnimations();
|
||||
void loadRooms();
|
||||
void createText();
|
||||
|
||||
// Métodos de limpieza
|
||||
void clear();
|
||||
void clearSounds();
|
||||
void clearMusics();
|
||||
|
||||
// Métodos de gestión de carga
|
||||
void load();
|
||||
void calculateTotal();
|
||||
void renderProgress();
|
||||
static void checkEvents();
|
||||
void updateLoadingProgress(int steps = 5);
|
||||
|
||||
// Helper para mensajes de error de carga
|
||||
[[noreturn]] static void throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e);
|
||||
|
||||
// Constructor y destructor
|
||||
Cache();
|
||||
~Cache() = default;
|
||||
|
||||
// Singleton instance
|
||||
static Cache* cache;
|
||||
|
||||
// Variables miembro
|
||||
std::vector<SoundResource> sounds_; // Vector con los sonidos
|
||||
std::vector<MusicResource> musics_; // Vector con las musicas
|
||||
std::vector<SurfaceResource> surfaces_; // Vector con las surfaces
|
||||
std::vector<ResourcePalette> palettes_; // Vector con las paletas
|
||||
std::vector<TextFileResource> text_files_; // Vector con los ficheros de texto
|
||||
std::vector<TextResource> texts_; // Vector con los objetos de texto
|
||||
std::vector<AnimationResource> animations_; // Vector con las animaciones
|
||||
std::vector<RoomResource> rooms_; // Vector con las habitaciones
|
||||
|
||||
ResourceCount count_{}; // Contador de recursos
|
||||
std::shared_ptr<Text> loading_text_; // Texto para la pantalla de carga
|
||||
};
|
||||
|
||||
} // namespace Resource
|
||||
182
source/core/resources/resource_helper.cpp
Normal file
182
source/core/resources/resource_helper.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
// resource_helper.cpp
|
||||
// Resource helper implementation
|
||||
|
||||
#include "resource_helper.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
#include "resource_loader.hpp"
|
||||
|
||||
namespace Resource::Helper {
|
||||
|
||||
static bool resource_system_initialized = false;
|
||||
|
||||
// Initialize the resource system
|
||||
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback)
|
||||
-> bool {
|
||||
if (resource_system_initialized) {
|
||||
std::cout << "ResourceHelper: Already initialized\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cout << "ResourceHelper: Initializing with pack: " << pack_file << '\n';
|
||||
std::cout << "ResourceHelper: Fallback enabled: " << (enable_fallback ? "Yes" : "No")
|
||||
<< '\n';
|
||||
|
||||
bool success = Loader::get().initialize(pack_file, enable_fallback);
|
||||
if (success) {
|
||||
resource_system_initialized = true;
|
||||
std::cout << "ResourceHelper: Initialization successful\n";
|
||||
} else {
|
||||
std::cerr << "ResourceHelper: Initialization failed\n";
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// Shutdown the resource system
|
||||
void shutdownResourceSystem() {
|
||||
if (resource_system_initialized) {
|
||||
Loader::get().shutdown();
|
||||
resource_system_initialized = false;
|
||||
std::cout << "ResourceHelper: Shutdown complete\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Load a file
|
||||
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
|
||||
if (!resource_system_initialized) {
|
||||
std::cerr << "ResourceHelper: System not initialized, loading from filesystem\n";
|
||||
// Fallback to direct filesystem access
|
||||
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);
|
||||
file.read(reinterpret_cast<char*>(data.data()), file_size);
|
||||
return data;
|
||||
}
|
||||
|
||||
// Determine if we should use the pack
|
||||
if (shouldUseResourcePack(filepath)) {
|
||||
// Convert to pack path
|
||||
std::string pack_path = getPackPath(filepath);
|
||||
|
||||
// Try to load from pack
|
||||
auto data = Loader::get().loadResource(pack_path);
|
||||
if (!data.empty()) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// If pack loading failed, try filesystem as fallback
|
||||
std::cerr << "ResourceHelper: Pack failed for " << pack_path
|
||||
<< ", trying filesystem\n";
|
||||
}
|
||||
|
||||
// Load from filesystem
|
||||
return Loader::get().loadResource(filepath);
|
||||
}
|
||||
|
||||
// Check if a file exists
|
||||
auto fileExists(const std::string& filepath) -> bool {
|
||||
if (!resource_system_initialized) {
|
||||
return std::filesystem::exists(filepath);
|
||||
}
|
||||
|
||||
// Check pack if appropriate
|
||||
if (shouldUseResourcePack(filepath)) {
|
||||
std::string pack_path = getPackPath(filepath);
|
||||
if (Loader::get().resourceExists(pack_path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check filesystem
|
||||
return std::filesystem::exists(filepath);
|
||||
}
|
||||
|
||||
// Convert asset path to pack path
|
||||
auto getPackPath(const std::string& asset_path) -> std::string {
|
||||
std::string path = asset_path;
|
||||
|
||||
// Convert backslashes to forward slashes
|
||||
std::ranges::replace(path, '\\', '/');
|
||||
|
||||
// If it's an absolute path containing "/data/", extract everything after "/data/"
|
||||
// This handles paths like: /Users/sergio/.../data/palette/file.pal -> palette/file.pal
|
||||
size_t data_pos = path.find("/data/");
|
||||
if (data_pos != std::string::npos) {
|
||||
return path.substr(data_pos + 6); // +6 to skip "/data/"
|
||||
}
|
||||
|
||||
// Remove leading slashes
|
||||
while (!path.empty() && path[0] == '/') {
|
||||
path = path.substr(1);
|
||||
}
|
||||
|
||||
// Remove "./" prefix if present
|
||||
if (path.starts_with("./")) {
|
||||
path = path.substr(2);
|
||||
}
|
||||
|
||||
// Remove "../" prefixes (for macOS bundle paths in development)
|
||||
while (path.starts_with("../")) {
|
||||
path = path.substr(3);
|
||||
}
|
||||
|
||||
// Remove "Resources/" prefix if present (for macOS bundle)
|
||||
const std::string RESOURCES_PREFIX = "Resources/";
|
||||
if (path.starts_with(RESOURCES_PREFIX)) {
|
||||
path = path.substr(RESOURCES_PREFIX.length());
|
||||
}
|
||||
|
||||
// Remove "data/" prefix if present
|
||||
const std::string DATA_PREFIX = "data/";
|
||||
if (path.starts_with(DATA_PREFIX)) {
|
||||
path = path.substr(DATA_PREFIX.length());
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// Check if file should use resource pack
|
||||
auto shouldUseResourcePack(const std::string& filepath) -> bool {
|
||||
std::string path = filepath;
|
||||
std::ranges::replace(path, '\\', '/');
|
||||
|
||||
// Don't use pack for most config files (except config/assets.yaml which is loaded
|
||||
// directly via Loader::loadAssetsConfig() in release builds)
|
||||
if (path.find("config/") != std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use pack for data files
|
||||
if (path.find("data/") != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it looks like a data file (has common extensions)
|
||||
if (path.find(".ogg") != std::string::npos || path.find(".wav") != std::string::npos ||
|
||||
path.find(".gif") != std::string::npos || path.find(".png") != std::string::npos ||
|
||||
path.find(".pal") != std::string::npos || path.find(".yaml") != std::string::npos ||
|
||||
path.find(".txt") != std::string::npos || path.find(".glsl") != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if pack is loaded
|
||||
auto isPackLoaded() -> bool {
|
||||
if (!resource_system_initialized) {
|
||||
return false;
|
||||
}
|
||||
return Loader::get().isPackLoaded();
|
||||
}
|
||||
|
||||
} // namespace Resource::Helper
|
||||
38
source/core/resources/resource_helper.hpp
Normal file
38
source/core/resources/resource_helper.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// resource_helper.hpp
|
||||
// Helper functions for resource loading (bridge to pack system)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Resource::Helper {
|
||||
|
||||
// Initialize the resource system
|
||||
// pack_file: Path to resources.pack
|
||||
// enable_fallback: Allow loading from filesystem if pack not available
|
||||
auto initializeResourceSystem(const std::string& pack_file = "resources.pack",
|
||||
bool enable_fallback = true) -> bool;
|
||||
|
||||
// Shutdown the resource system
|
||||
void shutdownResourceSystem();
|
||||
|
||||
// Load a file (tries pack first, then filesystem if fallback enabled)
|
||||
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
|
||||
|
||||
// Check if a file exists
|
||||
auto fileExists(const std::string& filepath) -> bool;
|
||||
|
||||
// Convert an asset path to a pack path
|
||||
// Example: "data/music/title.ogg" -> "music/title.ogg"
|
||||
auto getPackPath(const std::string& asset_path) -> std::string;
|
||||
|
||||
// Check if a file should use the resource pack
|
||||
// Returns false for config/ files (always from filesystem)
|
||||
auto shouldUseResourcePack(const std::string& filepath) -> bool;
|
||||
|
||||
// Check if pack is loaded
|
||||
auto isPackLoaded() -> bool;
|
||||
|
||||
} // namespace Resource::Helper
|
||||
446
source/core/resources/resource_list.cpp
Normal file
446
source/core/resources/resource_list.cpp
Normal file
@@ -0,0 +1,446 @@
|
||||
#include "core/resources/resource_list.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 ifstream, istringstream
|
||||
#include <iostream> // Para cout
|
||||
#include <sstream> // Para istringstream
|
||||
#include <stdexcept> // Para runtime_error
|
||||
|
||||
#include "external/fkyaml_node.hpp" // Para parsear YAML
|
||||
#include "utils/utils.hpp" // Para getFileName, printWithDots
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Singleton
|
||||
List* List::instance = nullptr;
|
||||
|
||||
void List::init(const std::string& executable_path) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
List::instance = new List(executable_path);
|
||||
}
|
||||
|
||||
void List::destroy() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
delete List::instance;
|
||||
}
|
||||
|
||||
auto List::get() -> List* {
|
||||
return List::instance;
|
||||
}
|
||||
|
||||
// Añade un elemento al mapa (función auxiliar)
|
||||
void List::addToMap(const std::string& file_path, Type type, bool required, bool absolute) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Warning: Asset '%s' already exists, overwriting",
|
||||
filename.c_str());
|
||||
}
|
||||
|
||||
file_list_.emplace(filename, Item{std::move(full_path), type, required});
|
||||
}
|
||||
|
||||
// Añade un elemento a la lista
|
||||
void List::add(const std::string& file_path, Type type, bool required, bool absolute) {
|
||||
addToMap(file_path, type, required, absolute);
|
||||
}
|
||||
|
||||
// Añade un asset al mapa y lo persiste en assets.yaml
|
||||
void List::addAsset(const std::string& path, Type type) {
|
||||
// Añadir al mapa en memoria
|
||||
addToMap(path, type, true, true);
|
||||
|
||||
// Persistir en assets.yaml
|
||||
if (config_file_path_.empty()) { return; }
|
||||
|
||||
std::ifstream in(config_file_path_);
|
||||
if (!in.is_open()) { return; }
|
||||
std::string content((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
in.close();
|
||||
|
||||
// Construir la ruta con variable ${PREFIX} (invertir la sustitución)
|
||||
std::string var_path = path;
|
||||
if (!prefix_.empty() && !executable_path_.empty()) {
|
||||
std::string full_prefix = executable_path_ + prefix_;
|
||||
auto pos = var_path.find(full_prefix);
|
||||
if (pos != std::string::npos) {
|
||||
var_path.replace(pos, full_prefix.length(), "${PREFIX}");
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar la última entrada con el mismo prefijo de ruta e insertar después
|
||||
std::string entry = " - " + var_path + "\n";
|
||||
auto last_pos = content.rfind(var_path.substr(0, var_path.rfind('/')));
|
||||
if (last_pos != std::string::npos) {
|
||||
auto end_of_line = content.find('\n', last_pos);
|
||||
if (end_of_line != std::string::npos) {
|
||||
content.insert(end_of_line + 1, entry);
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream out(config_file_path_);
|
||||
if (out.is_open()) {
|
||||
out << content;
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Quita un asset del mapa y lo elimina de assets.yaml
|
||||
void List::removeAsset(const std::string& filename) {
|
||||
// Obtener la ruta antes de borrar del mapa
|
||||
auto it = file_list_.find(filename);
|
||||
std::string file_path;
|
||||
if (it != file_list_.end()) {
|
||||
file_path = it->second.file;
|
||||
file_list_.erase(it);
|
||||
}
|
||||
|
||||
// Persistir en assets.yaml
|
||||
if (config_file_path_.empty() || file_path.empty()) { return; }
|
||||
|
||||
std::ifstream in(config_file_path_);
|
||||
if (!in.is_open()) { return; }
|
||||
std::string content((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
in.close();
|
||||
|
||||
// Construir la ruta con variable ${PREFIX}
|
||||
std::string var_path = file_path;
|
||||
if (!prefix_.empty() && !executable_path_.empty()) {
|
||||
std::string full_prefix = executable_path_ + prefix_;
|
||||
auto pos = var_path.find(full_prefix);
|
||||
if (pos != std::string::npos) {
|
||||
var_path.replace(pos, full_prefix.length(), "${PREFIX}");
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar la línea con el path y eliminarla
|
||||
auto pos = content.find(var_path);
|
||||
if (pos != std::string::npos) {
|
||||
auto line_start = content.rfind('\n', pos);
|
||||
line_start = (line_start == std::string::npos) ? 0 : line_start;
|
||||
auto line_end = content.find('\n', pos);
|
||||
if (line_end != std::string::npos) {
|
||||
content.erase(line_start, line_end - line_start);
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream out(config_file_path_);
|
||||
if (out.is_open()) {
|
||||
out << content;
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Carga recursos desde un archivo de configuración con soporte para variables
|
||||
void List::loadFromFile(const std::string& config_file_path, const std::string& prefix, const std::string& system_folder) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
config_file_path_ = config_file_path;
|
||||
prefix_ = prefix;
|
||||
|
||||
std::ifstream file(config_file_path);
|
||||
if (!file.is_open()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error: Cannot open config file: %s",
|
||||
config_file_path.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Read entire file into string
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
file.close();
|
||||
|
||||
// Parse using loadFromString
|
||||
loadFromString(buffer.str(), prefix, system_folder);
|
||||
}
|
||||
|
||||
// Carga recursos desde un string de configuración (para release con pack)
|
||||
void List::loadFromString(const std::string& config_content, const std::string& prefix, const std::string& system_folder) { // NOLINT(readability-convert-member-functions-to-static,readability-function-cognitive-complexity)
|
||||
try {
|
||||
// Parsear YAML
|
||||
auto yaml = fkyaml::node::deserialize(config_content);
|
||||
|
||||
// Verificar estructura básica
|
||||
if (!yaml.contains("assets")) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Invalid assets.yaml format - missing 'assets' key");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& assets = yaml["assets"];
|
||||
|
||||
// Iterar sobre cada categoría (fonts, palettes, etc.)
|
||||
for (auto it = assets.begin(); it != assets.end(); ++it) {
|
||||
const std::string& category = it.key().get_value<std::string>();
|
||||
const auto& category_assets = it.value();
|
||||
|
||||
if (category_assets.is_mapping()) {
|
||||
// Nuevo formato: categoría → { TIPO: [paths...], TIPO2: [paths...] }
|
||||
for (auto type_it = category_assets.begin(); type_it != category_assets.end(); ++type_it) {
|
||||
try {
|
||||
auto type_str = type_it.key().get_value<std::string>();
|
||||
Type type = parseAssetType(type_str);
|
||||
const auto& items = type_it.value();
|
||||
|
||||
if (!items.is_sequence()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Warning: Type '%s' in category '%s' is not a sequence, skipping",
|
||||
type_str.c_str(),
|
||||
category.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto& item : items) {
|
||||
try {
|
||||
if (item.is_string()) {
|
||||
// Formato simple: solo el path
|
||||
auto path = replaceVariables(item.get_value<std::string>(), prefix, system_folder);
|
||||
addToMap(path, type, true, false);
|
||||
} else if (item.is_mapping() && item.contains("path")) {
|
||||
// Formato expandido: { path, required?, absolute? }
|
||||
auto path = replaceVariables(item["path"].get_value<std::string>(), prefix, system_folder);
|
||||
bool required = !item.contains("required") || item["required"].get_value<bool>();
|
||||
bool absolute = item.contains("absolute") && item["absolute"].get_value<bool>();
|
||||
addToMap(path, type, required, absolute);
|
||||
} else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Warning: Invalid item in type '%s', category '%s', skipping",
|
||||
type_str.c_str(),
|
||||
category.c_str());
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error parsing asset in category '%s', type '%s': %s",
|
||||
category.c_str(),
|
||||
type_str.c_str(),
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error parsing type in category '%s': %s",
|
||||
category.c_str(),
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
} else if (category_assets.is_sequence()) {
|
||||
// Formato antiguo (retrocompatibilidad): categoría → [{type, path}, ...]
|
||||
for (const auto& asset : category_assets) {
|
||||
try {
|
||||
if (!asset.contains("type") || !asset.contains("path")) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Warning: Asset in category '%s' missing 'type' or 'path', skipping",
|
||||
category.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
auto type_str = asset["type"].get_value<std::string>();
|
||||
auto path = asset["path"].get_value<std::string>();
|
||||
bool required = true;
|
||||
bool absolute = false;
|
||||
|
||||
if (asset.contains("required")) {
|
||||
required = asset["required"].get_value<bool>();
|
||||
}
|
||||
if (asset.contains("absolute")) {
|
||||
absolute = asset["absolute"].get_value<bool>();
|
||||
}
|
||||
|
||||
path = replaceVariables(path, prefix, system_folder);
|
||||
Type type = parseAssetType(type_str);
|
||||
addToMap(path, type, required, absolute);
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error parsing asset in category '%s': %s",
|
||||
category.c_str(),
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Warning: Category '%s' has invalid format, skipping",
|
||||
category.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Loaded " << file_list_.size() << " assets from YAML config" << '\n';
|
||||
|
||||
} catch (const fkyaml::exception& e) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"YAML parsing error: %s",
|
||||
e.what());
|
||||
} catch (const std::exception& e) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error loading assets: %s",
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
|
||||
// Devuelve la ruta completa a un fichero (búsqueda O(1))
|
||||
auto List::get(const std::string& filename) const -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = file_list_.find(filename);
|
||||
if (it != file_list_.end()) {
|
||||
return it->second.file;
|
||||
}
|
||||
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: file %s not found", filename.c_str());
|
||||
return "";
|
||||
}
|
||||
|
||||
// Carga datos del archivo
|
||||
auto List::loadData(const std::string& filename) const -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = file_list_.find(filename);
|
||||
if (it != file_list_.end()) {
|
||||
std::ifstream file(it->second.file, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Warning: Could not open file %s for data loading",
|
||||
filename.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
// Obtener tamaño del archivo
|
||||
file.seekg(0, std::ios::end);
|
||||
size_t size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
// Leer datos
|
||||
std::vector<uint8_t> data(size);
|
||||
file.read(reinterpret_cast<char*>(data.data()), size);
|
||||
file.close();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: file %s not found for data loading", filename.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
// Verifica si un recurso existe
|
||||
auto List::exists(const std::string& filename) const -> bool {
|
||||
return file_list_.contains(filename);
|
||||
}
|
||||
|
||||
// Parsea string a Type
|
||||
auto List::parseAssetType(const std::string& type_str) -> Type { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (type_str == "DATA") {
|
||||
return Type::DATA;
|
||||
}
|
||||
if (type_str == "BITMAP") {
|
||||
return Type::BITMAP;
|
||||
}
|
||||
if (type_str == "ANIMATION") {
|
||||
return Type::ANIMATION;
|
||||
}
|
||||
if (type_str == "MUSIC") {
|
||||
return Type::MUSIC;
|
||||
}
|
||||
if (type_str == "SOUND") {
|
||||
return Type::SOUND;
|
||||
}
|
||||
if (type_str == "FONT") {
|
||||
return Type::FONT;
|
||||
}
|
||||
if (type_str == "ROOM") {
|
||||
return Type::ROOM;
|
||||
}
|
||||
if (type_str == "TILEMAP") {
|
||||
// TILEMAP está obsoleto, ahora todo es ROOM (.yaml unificado)
|
||||
return Type::ROOM;
|
||||
}
|
||||
if (type_str == "PALETTE") {
|
||||
return Type::PALETTE;
|
||||
}
|
||||
|
||||
throw std::runtime_error("Unknown asset type: " + type_str);
|
||||
}
|
||||
|
||||
// Devuelve el nombre del tipo de recurso
|
||||
auto List::getTypeName(Type type) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
switch (type) {
|
||||
case Type::DATA:
|
||||
return "DATA";
|
||||
case Type::BITMAP:
|
||||
return "BITMAP";
|
||||
case Type::ANIMATION:
|
||||
return "ANIMATION";
|
||||
case Type::MUSIC:
|
||||
return "MUSIC";
|
||||
case Type::SOUND:
|
||||
return "SOUND";
|
||||
case Type::FONT:
|
||||
return "FONT";
|
||||
case Type::ROOM:
|
||||
return "ROOM";
|
||||
case Type::PALETTE:
|
||||
return "PALETTE";
|
||||
default:
|
||||
return "ERROR";
|
||||
}
|
||||
}
|
||||
|
||||
// Devuelve la lista de recursos de un tipo
|
||||
auto List::getListByType(Type type) const -> std::vector<std::string> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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 List::replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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 List::parseOptions(const std::string& options, bool& required, bool& absolute) -> void { // NOLINT(readability-convert-member-functions-to-static)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Resource
|
||||
80
source/core/resources/resource_list.hpp
Normal file
80
source/core/resources/resource_list.hpp
Normal file
@@ -0,0 +1,80 @@
|
||||
#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
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// --- Clase List: gestor optimizado de recursos (singleton) ---
|
||||
class List {
|
||||
public:
|
||||
// --- Enums ---
|
||||
enum class Type : int {
|
||||
DATA, // Datos
|
||||
BITMAP, // Imágenes
|
||||
ANIMATION, // Animaciones
|
||||
MUSIC, // Música
|
||||
SOUND, // Sonidos
|
||||
FONT, // Fuentes
|
||||
ROOM, // Datos de habitación (.yaml - formato unificado con tilemap)
|
||||
PALETTE, // Paletas
|
||||
SIZE, // Tamaño (para iteración)
|
||||
};
|
||||
|
||||
// --- Métodos de singleton ---
|
||||
static void init(const std::string& executable_path);
|
||||
static void destroy();
|
||||
static auto get() -> List*;
|
||||
List(const List&) = delete;
|
||||
auto operator=(const List&) -> List& = 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 addAsset(const std::string& path, Type type); // Añade al mapa y persiste en assets.yaml
|
||||
void removeAsset(const std::string& filename); // Quita del mapa y persiste en assets.yaml
|
||||
void loadFromFile(const std::string& config_file_path, const std::string& prefix = "", const std::string& system_folder = ""); // Con soporte para variables
|
||||
void loadFromString(const std::string& config_content, const std::string& prefix = "", const std::string& system_folder = ""); // Para cargar desde pack (release)
|
||||
[[nodiscard]] auto get(const std::string& filename) const -> std::string; // Obtiene la ruta completa
|
||||
[[nodiscard]] auto loadData(const std::string& filename) const -> std::vector<uint8_t>; // Carga datos del archivo
|
||||
[[nodiscard]] auto getListByType(Type type) const -> std::vector<std::string>;
|
||||
[[nodiscard]] auto exists(const std::string& filename) const -> bool; // Verifica si un asset existe
|
||||
|
||||
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
|
||||
std::string config_file_path_; // Ruta del fichero assets.yaml
|
||||
std::string prefix_; // Prefijo para rutas (${PREFIX})
|
||||
|
||||
// --- Métodos internos ---
|
||||
[[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 List(std::string executable_path) // Constructor privado
|
||||
: executable_path_(std::move(executable_path)) {}
|
||||
~List() = default; // Destructor privado
|
||||
|
||||
// --- Instancia singleton ---
|
||||
static List* instance; // Instancia única de List
|
||||
};
|
||||
|
||||
} // namespace Resource
|
||||
199
source/core/resources/resource_loader.cpp
Normal file
199
source/core/resources/resource_loader.cpp
Normal file
@@ -0,0 +1,199 @@
|
||||
// resource_loader.cpp
|
||||
// Resource loader implementation
|
||||
|
||||
#include "resource_loader.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Get singleton instance
|
||||
auto Loader::get() -> Loader& {
|
||||
static Loader instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
// Initialize with a pack file
|
||||
auto Loader::initialize(const std::string& pack_file, bool enable_fallback)
|
||||
-> bool {
|
||||
if (initialized_) {
|
||||
std::cout << "Loader: Already initialized\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
fallback_to_files_ = enable_fallback;
|
||||
|
||||
// Try to load the pack file
|
||||
if (!pack_file.empty() && fileExistsOnFilesystem(pack_file)) {
|
||||
std::cout << "Loader: Loading pack file: " << pack_file << '\n';
|
||||
resource_pack_ = std::make_unique<Pack>();
|
||||
if (resource_pack_->loadPack(pack_file)) {
|
||||
std::cout << "Loader: Pack loaded successfully\n";
|
||||
initialized_ = true;
|
||||
return true;
|
||||
}
|
||||
std::cerr << "Loader: Failed to load pack file\n";
|
||||
resource_pack_.reset();
|
||||
} else {
|
||||
std::cout << "Loader: Pack file not found: " << pack_file << '\n';
|
||||
}
|
||||
|
||||
// If pack loading failed and fallback is disabled, fail
|
||||
if (!fallback_to_files_) {
|
||||
std::cerr << "Loader: Pack required but not found (fallback disabled)\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, fallback to filesystem
|
||||
std::cout << "Loader: Using filesystem fallback\n";
|
||||
initialized_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load a resource
|
||||
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> { // NOLINT(readability-make-member-function-const)
|
||||
if (!initialized_) {
|
||||
std::cerr << "Loader: Not initialized\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
// Try pack first if available
|
||||
if (resource_pack_ && resource_pack_->isLoaded()) {
|
||||
if (resource_pack_->hasResource(filename)) {
|
||||
auto data = resource_pack_->getResource(filename);
|
||||
if (!data.empty()) {
|
||||
return data;
|
||||
}
|
||||
std::cerr << "Loader: Failed to extract from pack: " << filename
|
||||
<< '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to filesystem if enabled
|
||||
if (fallback_to_files_) {
|
||||
return loadFromFilesystem(filename);
|
||||
}
|
||||
|
||||
std::cerr << "Loader: Resource not found: " << filename << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
// Check if a resource exists
|
||||
auto Loader::resourceExists(const std::string& filename) -> bool { // NOLINT(readability-make-member-function-const)
|
||||
if (!initialized_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check pack first
|
||||
if (resource_pack_ && resource_pack_->isLoaded()) {
|
||||
if (resource_pack_->hasResource(filename)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check filesystem if fallback enabled
|
||||
if (fallback_to_files_) {
|
||||
return fileExistsOnFilesystem(filename);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if pack is loaded
|
||||
auto Loader::isPackLoaded() const -> bool {
|
||||
return resource_pack_ && resource_pack_->isLoaded();
|
||||
}
|
||||
|
||||
// Get pack statistics
|
||||
auto Loader::getPackResourceCount() const -> size_t { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (resource_pack_ && resource_pack_->isLoaded()) {
|
||||
return resource_pack_->getResourceCount();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
void Loader::shutdown() {
|
||||
resource_pack_.reset();
|
||||
initialized_ = false;
|
||||
std::cout << "Loader: Shutdown complete\n";
|
||||
}
|
||||
|
||||
// Load from filesystem
|
||||
auto Loader::loadFromFilesystem(const std::string& filepath) // NOLINT(readability-convert-member-functions-to-static)
|
||||
-> std::vector<uint8_t> {
|
||||
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)) {
|
||||
std::cerr << "Loader: Failed to read file: " << filepath << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Check if file exists on filesystem
|
||||
auto Loader::fileExistsOnFilesystem(const std::string& filepath) -> bool {
|
||||
return std::filesystem::exists(filepath);
|
||||
}
|
||||
|
||||
// Validate pack integrity
|
||||
auto Loader::validatePack() const -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (!initialized_ || !resource_pack_ || !resource_pack_->isLoaded()) {
|
||||
std::cerr << "Loader: Cannot validate - pack not loaded\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate pack checksum
|
||||
uint32_t checksum = resource_pack_->calculatePackChecksum();
|
||||
|
||||
if (checksum == 0) {
|
||||
std::cerr << "Loader: Pack checksum is zero (invalid)\n";
|
||||
return false; // NOLINT(readability-simplify-boolean-expr)
|
||||
}
|
||||
|
||||
std::cout << "Loader: Pack checksum: 0x" << std::hex << checksum << std::dec
|
||||
<< '\n';
|
||||
std::cout << "Loader: Pack validation successful\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load assets.yaml from pack
|
||||
auto Loader::loadAssetsConfig() const -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (!initialized_ || !resource_pack_ || !resource_pack_->isLoaded()) {
|
||||
std::cerr << "Loader: Cannot load assets config - pack not loaded\n";
|
||||
return "";
|
||||
}
|
||||
|
||||
// Try to load config/assets.yaml from pack
|
||||
std::string config_path = "config/assets.yaml";
|
||||
|
||||
if (!resource_pack_->hasResource(config_path)) {
|
||||
std::cerr << "Loader: assets.yaml not found in pack: " << config_path << '\n';
|
||||
return "";
|
||||
}
|
||||
|
||||
auto data = resource_pack_->getResource(config_path);
|
||||
if (data.empty()) {
|
||||
std::cerr << "Loader: Failed to load assets.yaml from pack\n";
|
||||
return "";
|
||||
}
|
||||
|
||||
// Convert bytes to string
|
||||
std::string config_content(data.begin(), data.end());
|
||||
std::cout << "Loader: Loaded assets.yaml from pack (" << data.size()
|
||||
<< " bytes)\n";
|
||||
|
||||
return config_content;
|
||||
}
|
||||
|
||||
} // namespace Resource
|
||||
48
source/core/resources/resource_loader.hpp
Normal file
48
source/core/resources/resource_loader.hpp
Normal file
@@ -0,0 +1,48 @@
|
||||
// resource_loader.hpp
|
||||
// Singleton resource loader for managing pack and filesystem access
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "resource_pack.hpp"
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Singleton class for loading resources from pack or filesystem
|
||||
class Loader {
|
||||
public:
|
||||
static auto get() -> Loader&; // Singleton instance access
|
||||
|
||||
auto initialize(const std::string& pack_file, bool enable_fallback = true) -> bool; // Initialize loader with pack file
|
||||
|
||||
auto loadResource(const std::string& filename) -> std::vector<uint8_t>; // Load resource data
|
||||
auto resourceExists(const std::string& filename) -> bool; // Check resource availability
|
||||
|
||||
[[nodiscard]] auto isPackLoaded() const -> bool; // Pack status queries
|
||||
[[nodiscard]] auto getPackResourceCount() const -> size_t;
|
||||
[[nodiscard]] auto validatePack() const -> bool; // Validate pack integrity
|
||||
[[nodiscard]] auto loadAssetsConfig() const -> std::string; // Load assets.yaml from pack
|
||||
|
||||
void shutdown(); // Cleanup
|
||||
|
||||
Loader(const Loader&) = delete; // Deleted copy/move constructors
|
||||
auto operator=(const Loader&) -> Loader& = delete;
|
||||
Loader(Loader&&) = delete;
|
||||
auto operator=(Loader&&) -> Loader& = delete;
|
||||
|
||||
private:
|
||||
Loader() = default;
|
||||
~Loader() = default;
|
||||
|
||||
static auto loadFromFilesystem(const std::string& filepath) -> std::vector<uint8_t>; // Filesystem helpers
|
||||
static auto fileExistsOnFilesystem(const std::string& filepath) -> bool;
|
||||
|
||||
std::unique_ptr<Pack> resource_pack_; // Member variables
|
||||
bool fallback_to_files_{true};
|
||||
bool initialized_{false};
|
||||
};
|
||||
|
||||
} // namespace Resource
|
||||
303
source/core/resources/resource_pack.cpp
Normal file
303
source/core/resources/resource_pack.cpp
Normal file
@@ -0,0 +1,303 @@
|
||||
// resource_pack.cpp
|
||||
// Resource pack implementation for Projecte 2026
|
||||
|
||||
#include "resource_pack.hpp"
|
||||
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Calculate CRC32 checksum for data verification
|
||||
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t { // NOLINT(readability-convert-member-functions-to-static)
|
||||
uint32_t checksum = 0x12345678;
|
||||
for (unsigned char byte : data) {
|
||||
checksum = ((checksum << 5) + checksum) + byte;
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
|
||||
// XOR encryption (symmetric - same function for encrypt/decrypt)
|
||||
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) { // NOLINT(readability-identifier-naming)
|
||||
if (key.empty()) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
data[i] ^= key[i % key.length()];
|
||||
}
|
||||
}
|
||||
|
||||
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) { // NOLINT(readability-identifier-naming)
|
||||
// XOR is symmetric
|
||||
encryptData(data, key);
|
||||
}
|
||||
|
||||
// Read entire file into memory
|
||||
auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
std::cerr << "ResourcePack: Failed to open file: " << filepath << '\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 << "ResourcePack: Failed to read file: " << filepath << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Add a single file to the pack
|
||||
auto Pack::addFile(const std::string& filepath, const std::string& pack_name) // NOLINT(readability-convert-member-functions-to-static)
|
||||
-> bool {
|
||||
auto file_data = readFile(filepath);
|
||||
if (file_data.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ResourceEntry entry{
|
||||
.filename = pack_name,
|
||||
.offset = data_.size(),
|
||||
.size = file_data.size(),
|
||||
.checksum = calculateChecksum(file_data)};
|
||||
|
||||
// Append file data to the data block
|
||||
data_.insert(data_.end(), file_data.begin(), file_data.end());
|
||||
|
||||
resources_[pack_name] = entry;
|
||||
|
||||
std::cout << "Added: " << pack_name << " (" << file_data.size() << " bytes)\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add all files from a directory recursively
|
||||
auto Pack::addDirectory(const std::string& dir_path, // NOLINT(readability-convert-member-functions-to-static)
|
||||
const std::string& base_path) -> bool {
|
||||
namespace fs = std::filesystem; // NOLINT(readability-identifier-naming)
|
||||
|
||||
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
|
||||
std::cerr << "ResourcePack: Directory not found: " << dir_path << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string current_base = base_path.empty() ? "" : base_path + "/";
|
||||
|
||||
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
|
||||
if (!entry.is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string full_path = entry.path().string();
|
||||
std::string relative_path = entry.path().lexically_relative(dir_path).string();
|
||||
|
||||
// Convert backslashes to forward slashes (Windows compatibility)
|
||||
std::ranges::replace(relative_path, '\\', '/');
|
||||
|
||||
// Skip development files
|
||||
if (relative_path.find(".world") != std::string::npos ||
|
||||
relative_path.find(".tsx") != std::string::npos) {
|
||||
std::cout << "Skipping development file: " << relative_path << '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string pack_name = current_base + relative_path;
|
||||
addFile(full_path, pack_name);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save the pack to a file
|
||||
auto Pack::savePack(const std::string& pack_file) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::ofstream file(pack_file, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "ResourcePack: Failed to create pack file: " << pack_file << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write header
|
||||
file.write(MAGIC_HEADER.data(), MAGIC_HEADER.size());
|
||||
file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION));
|
||||
|
||||
// Write resource count
|
||||
auto resource_count = static_cast<uint32_t>(resources_.size());
|
||||
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
|
||||
|
||||
// Write resource entries
|
||||
for (const auto& [name, entry] : resources_) {
|
||||
// Write filename length and name
|
||||
auto name_len = static_cast<uint32_t>(entry.filename.length());
|
||||
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
|
||||
file.write(entry.filename.c_str(), name_len);
|
||||
|
||||
// Write offset, size, checksum
|
||||
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));
|
||||
}
|
||||
|
||||
// Encrypt data
|
||||
std::vector<uint8_t> encrypted_data = data_;
|
||||
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
|
||||
|
||||
// Write encrypted data size and data
|
||||
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);
|
||||
|
||||
std::cout << "\nPack saved successfully: " << pack_file << '\n';
|
||||
std::cout << "Resources: " << resource_count << '\n';
|
||||
std::cout << "Total size: " << data_size << " bytes\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load a pack from a file
|
||||
auto Pack::loadPack(const std::string& pack_file) -> bool {
|
||||
std::ifstream file(pack_file, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "ResourcePack: Failed to open pack file: " << pack_file << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read and verify header
|
||||
std::array<char, 4> header{};
|
||||
file.read(header.data(), header.size());
|
||||
if (header != MAGIC_HEADER) {
|
||||
std::cerr << "ResourcePack: Invalid pack header\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read and verify version
|
||||
uint32_t version = 0;
|
||||
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
||||
if (version != VERSION) {
|
||||
std::cerr << "ResourcePack: Unsupported pack version: " << version << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read resource count
|
||||
uint32_t resource_count = 0;
|
||||
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
|
||||
|
||||
// Read resource entries
|
||||
resources_.clear();
|
||||
for (uint32_t i = 0; i < resource_count; ++i) {
|
||||
// Read filename
|
||||
uint32_t name_len = 0;
|
||||
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
|
||||
|
||||
std::string filename(name_len, '\0');
|
||||
file.read(filename.data(), name_len);
|
||||
|
||||
// Read entry data
|
||||
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;
|
||||
}
|
||||
|
||||
// Read encrypted data
|
||||
uint64_t data_size = 0;
|
||||
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
|
||||
|
||||
data_.resize(data_size);
|
||||
file.read(reinterpret_cast<char*>(data_.data()), data_size);
|
||||
|
||||
// Decrypt data
|
||||
decryptData(data_, DEFAULT_ENCRYPT_KEY);
|
||||
|
||||
loaded_ = true;
|
||||
|
||||
std::cout << "ResourcePack loaded: " << pack_file << '\n';
|
||||
std::cout << "Resources: " << resource_count << '\n';
|
||||
std::cout << "Data size: " << data_size << " bytes\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get a resource by name
|
||||
auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto it = resources_.find(filename);
|
||||
if (it == resources_.end()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const ResourceEntry& entry = it->second;
|
||||
|
||||
// Extract data slice
|
||||
if (entry.offset + entry.size > data_.size()) {
|
||||
std::cerr << "ResourcePack: Invalid offset/size for: " << filename << '\n';
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> result(data_.begin() + entry.offset,
|
||||
data_.begin() + entry.offset + entry.size);
|
||||
|
||||
// Verify checksum
|
||||
uint32_t checksum = calculateChecksum(result);
|
||||
if (checksum != entry.checksum) {
|
||||
std::cerr << "ResourcePack: Checksum mismatch for: " << filename << '\n';
|
||||
std::cerr << " Expected: 0x" << std::hex << entry.checksum << '\n';
|
||||
std::cerr << " Got: 0x" << std::hex << checksum << std::dec << '\n';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if a resource exists
|
||||
auto Pack::hasResource(const std::string& filename) const -> bool {
|
||||
return resources_.contains(filename);
|
||||
}
|
||||
|
||||
// Get list of all resources
|
||||
auto Pack::getResourceList() const -> std::vector<std::string> { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::vector<std::string> list;
|
||||
list.reserve(resources_.size());
|
||||
for (const auto& [name, entry] : resources_) {
|
||||
list.push_back(name);
|
||||
}
|
||||
std::ranges::sort(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
// Calculate overall pack checksum for validation
|
||||
auto Pack::calculatePackChecksum() const -> uint32_t { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (!loaded_ || data_.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Combine checksums of all resources for a global checksum
|
||||
uint32_t global_checksum = 0x87654321;
|
||||
|
||||
// Sort resources by name for deterministic checksum
|
||||
std::vector<std::string> sorted_names;
|
||||
sorted_names.reserve(resources_.size());
|
||||
for (const auto& [name, entry] : resources_) {
|
||||
sorted_names.push_back(name);
|
||||
}
|
||||
std::ranges::sort(sorted_names);
|
||||
|
||||
// Combine individual checksums
|
||||
for (const auto& name : sorted_names) {
|
||||
const auto& entry = resources_.at(name);
|
||||
global_checksum = ((global_checksum << 5) + global_checksum) + entry.checksum;
|
||||
global_checksum = ((global_checksum << 5) + global_checksum) + entry.size;
|
||||
}
|
||||
|
||||
return global_checksum;
|
||||
}
|
||||
|
||||
} // namespace Resource
|
||||
68
source/core/resources/resource_pack.hpp
Normal file
68
source/core/resources/resource_pack.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
// resource_pack.hpp
|
||||
// Resource pack file format and management for Projecte 2026
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Resource {
|
||||
|
||||
// Entry metadata for each resource in the pack
|
||||
struct ResourceEntry {
|
||||
std::string filename; // Relative path within pack
|
||||
uint64_t offset{0}; // Byte offset in data block
|
||||
uint64_t size{0}; // Size in bytes
|
||||
uint32_t checksum{0}; // CRC32 checksum for verification
|
||||
};
|
||||
|
||||
// Resource pack file format
|
||||
// Header: "JDDI" (4 bytes) + Version (4 bytes)
|
||||
// Metadata: Count + array of ResourceEntry
|
||||
// Data: Encrypted data block
|
||||
class Pack {
|
||||
public:
|
||||
Pack() = default;
|
||||
~Pack() = default;
|
||||
|
||||
Pack(const Pack&) = delete; // Deleted copy/move constructors
|
||||
auto operator=(const Pack&) -> Pack& = delete;
|
||||
Pack(Pack&&) = delete;
|
||||
auto operator=(Pack&&) -> Pack& = delete;
|
||||
|
||||
auto addFile(const std::string& filepath, const std::string& pack_name) -> bool; // Building packs
|
||||
auto addDirectory(const std::string& dir_path, const std::string& base_path = "") -> bool;
|
||||
|
||||
auto savePack(const std::string& pack_file) -> bool; // Pack I/O
|
||||
auto loadPack(const std::string& pack_file) -> bool;
|
||||
|
||||
auto getResource(const std::string& filename) -> std::vector<uint8_t>; // Resource access
|
||||
[[nodiscard]] auto hasResource(const std::string& filename) const -> bool;
|
||||
[[nodiscard]] auto getResourceList() const -> std::vector<std::string>;
|
||||
|
||||
[[nodiscard]] auto isLoaded() const -> bool { return loaded_; } // Status queries
|
||||
[[nodiscard]] auto getResourceCount() const -> size_t { return resources_.size(); }
|
||||
[[nodiscard]] auto getDataSize() const -> size_t { return data_.size(); }
|
||||
[[nodiscard]] auto calculatePackChecksum() const -> uint32_t; // Validation
|
||||
|
||||
private:
|
||||
static constexpr std::array<char, 4> MAGIC_HEADER = {'J', 'D', 'D', 'I'}; // Pack format constants
|
||||
static constexpr uint32_t VERSION = 1;
|
||||
static constexpr const char* DEFAULT_ENCRYPT_KEY = "JDDI_RESOURCES_2024";
|
||||
|
||||
static auto calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t; // Utility methods
|
||||
|
||||
static void encryptData(std::vector<uint8_t>& data, const std::string& key); // Encryption/decryption
|
||||
static void decryptData(std::vector<uint8_t>& data, const std::string& key);
|
||||
|
||||
static auto readFile(const std::string& filepath) -> std::vector<uint8_t>; // File I/O
|
||||
|
||||
std::unordered_map<std::string, ResourceEntry> resources_; // Member variables
|
||||
std::vector<uint8_t> data_; // Encrypted data block
|
||||
bool loaded_{false};
|
||||
};
|
||||
|
||||
} // namespace Resource
|
||||
62
source/core/resources/resource_types.hpp
Normal file
62
source/core/resources/resource_types.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint> // Para uint8_t
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/rendering/surface.hpp" // Para Palette y Surface
|
||||
#include "core/rendering/text.hpp" // Para Text y Text::File
|
||||
#include "game/gameplay/room.hpp" // Para Room::Data
|
||||
|
||||
// Forward declarations
|
||||
struct JA_Music_t;
|
||||
struct JA_Sound_t;
|
||||
|
||||
// Estructura para almacenar ficheros de sonido y su nombre
|
||||
struct SoundResource {
|
||||
std::string name; // Nombre del sonido
|
||||
JA_Sound_t* sound{nullptr}; // Objeto con el sonido
|
||||
};
|
||||
|
||||
// Estructura para almacenar ficheros musicales y su nombre
|
||||
struct MusicResource {
|
||||
std::string name; // Nombre de la musica
|
||||
JA_Music_t* music{nullptr}; // Objeto con la música
|
||||
};
|
||||
|
||||
// Estructura para almacenar objetos Surface y su nombre
|
||||
struct SurfaceResource {
|
||||
std::string name; // Nombre de la surface
|
||||
std::shared_ptr<Surface> surface; // Objeto con la surface
|
||||
};
|
||||
|
||||
// Estructura para almacenar objetos Palette y su nombre
|
||||
struct ResourcePalette {
|
||||
std::string name; // Nombre de la surface
|
||||
Palette palette{}; // Paleta
|
||||
};
|
||||
|
||||
// Estructura para almacenar ficheros TextFile y su nombre
|
||||
struct TextFileResource {
|
||||
std::string name; // Nombre del fichero
|
||||
std::shared_ptr<Text::File> text_file; // Objeto con los descriptores de la fuente de texto
|
||||
};
|
||||
|
||||
// Estructura para almacenar objetos Text y su nombre
|
||||
struct TextResource {
|
||||
std::string name; // Nombre del objeto
|
||||
std::shared_ptr<Text> text; // Objeto
|
||||
};
|
||||
|
||||
// Estructura para almacenar ficheros animaciones y su nombre
|
||||
struct AnimationResource {
|
||||
std::string name; // Nombre del fichero
|
||||
std::vector<uint8_t> yaml_data; // Bytes del archivo YAML sin parsear
|
||||
};
|
||||
|
||||
// Estructura para almacenar habitaciones y su nombre
|
||||
struct RoomResource {
|
||||
std::string name; // Nombre de la habitación
|
||||
std::shared_ptr<Room::Data> room; // Habitación
|
||||
};
|
||||
189
source/core/system/debug.cpp
Normal file
189
source/core/system/debug.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
#include "core/system/debug.hpp"
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include <algorithm> // Para max
|
||||
#include <fstream> // Para ifstream, ofstream
|
||||
#include <memory> // Para __shared_ptr_access, shared_ptr
|
||||
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||
#include "game/defaults.hpp" // Para Defaults::Game::*
|
||||
#include "utils/defines.hpp" // Para Tile::SIZE
|
||||
#include "utils/utils.hpp" // Para Color, Flip::
|
||||
|
||||
// [SINGLETON]
|
||||
Debug* Debug::debug = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||
void Debug::init() {
|
||||
Debug::debug = new Debug();
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
void Debug::destroy() {
|
||||
delete Debug::debug;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto Debug::get() -> Debug* {
|
||||
return Debug::debug;
|
||||
}
|
||||
|
||||
// Dibuja en pantalla
|
||||
void Debug::render() { // NOLINT(readability-make-member-function-const)
|
||||
auto text = Resource::Cache::get()->getText("aseprite");
|
||||
int y = y_;
|
||||
int w = 0;
|
||||
constexpr int DESP_Y = 7;
|
||||
const int CHAR_SIZE = text->getCharacterSize();
|
||||
|
||||
// Watch window: valores persistentes (key: value)
|
||||
for (const auto& [key, value] : watches_) {
|
||||
const std::string LINE = key + ": " + value;
|
||||
text->write(x_, y, LINE);
|
||||
w = std::max(w, text->length(LINE));
|
||||
y += DESP_Y;
|
||||
if (y > 192 - CHAR_SIZE) {
|
||||
y = y_;
|
||||
x_ += w + 2;
|
||||
w = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Slot one-shot: mensajes de un solo frame
|
||||
for (const auto& s : slot_) {
|
||||
text->write(x_, y, s);
|
||||
w = std::max(w, text->length(s));
|
||||
y += DESP_Y;
|
||||
if (y > 192 - CHAR_SIZE) {
|
||||
y = y_;
|
||||
x_ += w + 2;
|
||||
w = 0;
|
||||
}
|
||||
}
|
||||
|
||||
y = 0;
|
||||
for (const auto& l : log_) {
|
||||
text->writeColored(x_ + 10, y, l, static_cast<Uint8>(PaletteColor::WHITE));
|
||||
y += CHAR_SIZE + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Establece/actualiza un valor persistente en el watch window
|
||||
void Debug::set(const std::string& key, const std::string& value) {
|
||||
watches_[key] = value;
|
||||
}
|
||||
|
||||
// Elimina un valor del watch window
|
||||
void Debug::unset(const std::string& key) {
|
||||
watches_.erase(key);
|
||||
}
|
||||
|
||||
// Establece la posición donde se colocará la información de debug
|
||||
void Debug::setPos(SDL_FPoint p) {
|
||||
x_ = p.x;
|
||||
y_ = p.y;
|
||||
}
|
||||
|
||||
// Establece la ruta del archivo debug.yaml
|
||||
void Debug::setDebugFile(const std::string& path) {
|
||||
debug_file_path_ = path;
|
||||
}
|
||||
|
||||
// Convierte string a SceneManager::Scene (para debug.yaml)
|
||||
static auto sceneFromString(const std::string& s) -> SceneManager::Scene {
|
||||
if (s == "LOGO") { return SceneManager::Scene::LOGO; }
|
||||
if (s == "LOADING") { return SceneManager::Scene::LOADING_SCREEN; }
|
||||
if (s == "TITLE") { return SceneManager::Scene::TITLE; }
|
||||
if (s == "CREDITS") { return SceneManager::Scene::CREDITS; }
|
||||
if (s == "DEMO") { return SceneManager::Scene::DEMO; }
|
||||
if (s == "ENDING") { return SceneManager::Scene::ENDING; }
|
||||
if (s == "ENDING2") { return SceneManager::Scene::ENDING2; }
|
||||
return SceneManager::Scene::GAME; // Fallback seguro
|
||||
}
|
||||
|
||||
// Convierte SceneManager::Scene a string (para debug.yaml)
|
||||
static auto sceneToString(SceneManager::Scene scene) -> std::string {
|
||||
switch (scene) {
|
||||
case SceneManager::Scene::LOGO:
|
||||
return "LOGO";
|
||||
case SceneManager::Scene::LOADING_SCREEN:
|
||||
return "LOADING";
|
||||
case SceneManager::Scene::TITLE:
|
||||
return "TITLE";
|
||||
case SceneManager::Scene::CREDITS:
|
||||
return "CREDITS";
|
||||
case SceneManager::Scene::DEMO:
|
||||
return "DEMO";
|
||||
case SceneManager::Scene::ENDING:
|
||||
return "ENDING";
|
||||
case SceneManager::Scene::ENDING2:
|
||||
return "ENDING2";
|
||||
default:
|
||||
return "GAME";
|
||||
}
|
||||
}
|
||||
|
||||
// Carga la configuración de debug desde debug.yaml
|
||||
void Debug::loadFromFile() {
|
||||
// Inicializar con valores de release por defecto
|
||||
spawn_settings_.room = Defaults::Game::Room::INITIAL;
|
||||
spawn_settings_.spawn_x = Defaults::Game::Player::SPAWN_X;
|
||||
spawn_settings_.spawn_y = Defaults::Game::Player::SPAWN_Y;
|
||||
spawn_settings_.flip = Defaults::Game::Player::SPAWN_FLIP;
|
||||
initial_scene_ = SceneManager::Scene::GAME;
|
||||
|
||||
std::ifstream file(debug_file_path_);
|
||||
if (!file.good()) {
|
||||
saveToFile(); // No existe: crear con valores por defecto
|
||||
return;
|
||||
}
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
if (yaml.contains("room")) {
|
||||
spawn_settings_.room = yaml["room"].get_value<std::string>();
|
||||
}
|
||||
if (yaml.contains("spawn_x")) {
|
||||
spawn_settings_.spawn_x = yaml["spawn_x"].get_value<int>() * Tile::SIZE;
|
||||
}
|
||||
if (yaml.contains("spawn_y")) {
|
||||
spawn_settings_.spawn_y = yaml["spawn_y"].get_value<int>() * Tile::SIZE;
|
||||
}
|
||||
if (yaml.contains("spawn_flip")) {
|
||||
auto s = yaml["spawn_flip"].get_value<std::string>();
|
||||
spawn_settings_.flip = (s == "right") ? Flip::RIGHT : Flip::LEFT;
|
||||
}
|
||||
if (yaml.contains("initial_scene")) {
|
||||
initial_scene_ = sceneFromString(yaml["initial_scene"].get_value<std::string>());
|
||||
}
|
||||
} catch (...) {
|
||||
// YAML inválido: resetear a defaults y sobreescribir
|
||||
spawn_settings_.room = Defaults::Game::Room::INITIAL;
|
||||
spawn_settings_.spawn_x = Defaults::Game::Player::SPAWN_X;
|
||||
spawn_settings_.spawn_y = Defaults::Game::Player::SPAWN_Y;
|
||||
spawn_settings_.flip = Defaults::Game::Player::SPAWN_FLIP;
|
||||
initial_scene_ = SceneManager::Scene::GAME;
|
||||
saveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
// Guarda la configuración de debug en debug.yaml
|
||||
void Debug::saveToFile() const {
|
||||
std::ofstream file(debug_file_path_);
|
||||
if (!file.is_open()) { return; }
|
||||
file << "# Projecte 2026 - Debug Configuration\n";
|
||||
file << "# Edita para cambiar la habitacion y spawn del jugador en builds debug.\n\n";
|
||||
file << "room: \"" << spawn_settings_.room << "\"\n";
|
||||
file << "spawn_x: " << (spawn_settings_.spawn_x / Tile::SIZE) << " # en tiles\n";
|
||||
file << "spawn_y: " << (spawn_settings_.spawn_y / Tile::SIZE) << " # en tiles\n";
|
||||
file << "spawn_flip: " << ((spawn_settings_.flip == Flip::RIGHT) ? "right" : "left") << "\n";
|
||||
file << "initial_scene: " << sceneToString(initial_scene_) << "\n";
|
||||
}
|
||||
|
||||
#endif // _DEBUG
|
||||
69
source/core/system/debug.hpp
Normal file
69
source/core/system/debug.hpp
Normal file
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <map> // Para map
|
||||
#include <string> // Para string
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "game/scene_manager.hpp" // Para SceneManager::Scene
|
||||
|
||||
// Clase Debug
|
||||
class Debug {
|
||||
public:
|
||||
struct SpawnSettings {
|
||||
std::string room;
|
||||
int spawn_x = 0;
|
||||
int spawn_y = 0;
|
||||
SDL_FlipMode flip = SDL_FLIP_NONE;
|
||||
};
|
||||
|
||||
static void init(); // [SINGLETON] Crearemos el objeto con esta función estática
|
||||
static void destroy(); // [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
static auto get() -> Debug*; // [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
|
||||
void render(); // Dibuja en pantalla
|
||||
|
||||
void setPos(SDL_FPoint p); // Establece la posición donde se colocará la información de debug
|
||||
|
||||
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; } // Obtiene si el debug está activo
|
||||
|
||||
void add(const std::string& text) { slot_.push_back(text); } // Añade texto one-shot al slot (se limpia cada frame)
|
||||
void clear() { slot_.clear(); } // Limpia el slot one-shot (no afecta a watches)
|
||||
void addToLog(const std::string& text) { log_.push_back(text); } // Añade texto al log
|
||||
void clearLog() { log_.clear(); } // Limpia el log
|
||||
void set(const std::string& key, const std::string& value); // Establece/actualiza un valor persistente en el watch window
|
||||
void unset(const std::string& key); // Elimina un valor del watch window
|
||||
void clearWatches() { watches_.clear(); } // Limpia todos los watches
|
||||
void setEnabled(bool value) { enabled_ = value; } // Establece si el debug está activo
|
||||
void toggleEnabled() { enabled_ = !enabled_; } // Alterna el estado del debug
|
||||
|
||||
void setDebugFile(const std::string& path); // Establece la ruta del archivo debug.yaml
|
||||
void loadFromFile(); // Carga la configuración de debug desde debug.yaml
|
||||
void saveToFile() const; // Guarda la configuración de debug en debug.yaml
|
||||
[[nodiscard]] auto getSpawnSettings() const -> const SpawnSettings& { return spawn_settings_; } // Obtiene los valores de spawn
|
||||
void setSpawnSettings(const SpawnSettings& s) { spawn_settings_ = s; } // Establece los valores de spawn
|
||||
[[nodiscard]] auto getInitialScene() const -> SceneManager::Scene { return initial_scene_; } // Obtiene la escena inicial de debug
|
||||
void setInitialScene(SceneManager::Scene s) { initial_scene_ = s; } // Establece la escena inicial de debug
|
||||
|
||||
private:
|
||||
static Debug* debug; // [SINGLETON] Objeto privado
|
||||
|
||||
Debug() = default; // Constructor
|
||||
~Debug() = default; // Destructor
|
||||
|
||||
// Variables
|
||||
std::map<std::string, std::string> watches_; // Watch window: valores persistentes (key→value)
|
||||
std::vector<std::string> slot_; // One-shot: textos que se limpian cada frame
|
||||
std::vector<std::string> log_; // Log persistente
|
||||
int x_ = 0; // Posicion donde escribir el texto de debug
|
||||
int y_ = 0; // Posición donde escribir el texto de debug
|
||||
bool enabled_ = false; // Indica si esta activo el modo debug
|
||||
std::string debug_file_path_; // Ruta del archivo debug.yaml
|
||||
SpawnSettings spawn_settings_; // Configuración de spawn para debug
|
||||
SceneManager::Scene initial_scene_ = SceneManager::Scene::GAME; // Escena inicial en debug
|
||||
};
|
||||
|
||||
#endif // _DEBUG
|
||||
428
source/core/system/director.cpp
Normal file
428
source/core/system/director.cpp
Normal file
@@ -0,0 +1,428 @@
|
||||
#include "core/system/director.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <sys/stat.h> // Para mkdir, stat, S_IRWXU
|
||||
#include <unistd.h> // Para getuid
|
||||
|
||||
#include <cerrno> // Para errno, EEXIST, EACCES, ENAMETOO...
|
||||
#include <cstdio> // Para printf, perror
|
||||
#include <cstdlib> // Para exit, EXIT_FAILURE, srand
|
||||
#include <iostream> // Para basic_ostream, operator<<, cout
|
||||
#include <memory> // Para make_unique, unique_ptr
|
||||
#include <string> // Para operator+, allocator, char_traits
|
||||
|
||||
#include "core/audio/audio.hpp" // Para Audio
|
||||
#include "core/input/input.hpp" // Para Input, InputAction
|
||||
#include "core/locale/locale.hpp" // Para Locale
|
||||
#include "core/rendering/render_info.hpp" // Para RenderInfo
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
|
||||
#include "core/resources/resource_list.hpp" // Para Asset, AssetType
|
||||
#include "core/resources/resource_loader.hpp" // Para ResourceLoader
|
||||
#include "game/gameplay/cheevos.hpp" // Para Cheevos
|
||||
#include "game/options.hpp" // Para Options, options, OptionsVideo
|
||||
#include "game/scene_manager.hpp" // Para SceneManager
|
||||
#include "game/scenes/credits.hpp" // Para Credits
|
||||
#include "game/scenes/ending.hpp" // Para Ending
|
||||
#include "game/scenes/ending2.hpp" // Para Ending2
|
||||
#include "game/scenes/game.hpp" // Para Game, GameMode
|
||||
#include "game/scenes/game_over.hpp" // Para GameOver
|
||||
#include "game/scenes/loading_screen.hpp" // Para LoadingScreen
|
||||
#include "game/scenes/logo.hpp" // Para Logo
|
||||
#include "game/scenes/title.hpp" // Para Title
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
#include "utils/defines.hpp" // Para WINDOW_CAPTION
|
||||
|
||||
#ifdef _DEBUG
|
||||
#include "core/system/debug.hpp" // Para Debug
|
||||
#include "game/editor/map_editor.hpp" // Para MapEditor
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <pwd.h>
|
||||
#endif
|
||||
|
||||
// Constructor
|
||||
Director::Director() {
|
||||
std::cout << "Game start" << '\n';
|
||||
|
||||
// Obtiene la ruta del ejecutable
|
||||
std::string base = SDL_GetBasePath();
|
||||
if (!base.empty() && base.back() == '/') {
|
||||
base.pop_back();
|
||||
}
|
||||
executable_path_ = base;
|
||||
|
||||
// Crea la carpeta del sistema donde guardar datos
|
||||
createSystemFolder("jailgames");
|
||||
createSystemFolder("jailgames/projecte_2026");
|
||||
|
||||
// Crea el subdirectorio shaders/ dentro de system_folder_ sin modificar system_folder_
|
||||
{
|
||||
std::string shaders_dir = system_folder_ + "/shaders";
|
||||
struct stat st = {.st_dev = 0};
|
||||
if (stat(shaders_dir.c_str(), &st) == -1) {
|
||||
errno = 0;
|
||||
#ifdef _WIN32
|
||||
mkdir(shaders_dir.c_str());
|
||||
#else
|
||||
mkdir(shaders_dir.c_str(), S_IRWXU);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Determinar el prefijo de ruta según la plataforma
|
||||
#ifdef MACOS_BUNDLE
|
||||
const std::string PREFIX = "/../Resources";
|
||||
#else
|
||||
const std::string PREFIX;
|
||||
#endif
|
||||
|
||||
// Preparar ruta al pack (en macOS bundle está en Contents/Resources/)
|
||||
std::string pack_path = executable_path_ + PREFIX + "/resources.pack";
|
||||
|
||||
#ifdef RELEASE_BUILD
|
||||
// ============================================================
|
||||
// RELEASE BUILD: Pack-first architecture
|
||||
// ============================================================
|
||||
std::cout << "\n** RELEASE MODE: Pack-first initialization\n";
|
||||
|
||||
// 1. Initialize resource pack system (required, no fallback)
|
||||
std::cout << "Initializing resource pack: " << pack_path << '\n';
|
||||
if (!Resource::Helper::initializeResourceSystem(pack_path, false)) {
|
||||
std::cerr << "ERROR: Failed to load resources.pack (required in release builds)\n";
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// 2. Validate pack integrity
|
||||
std::cout << "Validating pack integrity..." << '\n';
|
||||
if (!Resource::Loader::get().validatePack()) {
|
||||
std::cerr << "ERROR: Pack validation failed\n";
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// 3. Load assets.yaml from pack
|
||||
std::cout << "Loading assets configuration from pack..." << '\n';
|
||||
std::string assets_config = Resource::Loader::get().loadAssetsConfig();
|
||||
if (assets_config.empty()) {
|
||||
std::cerr << "ERROR: Failed to load assets.yaml from pack\n";
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// 4. Initialize Asset system with config from pack
|
||||
// NOTE: In release, don't use executable_path or PREFIX - paths in pack are relative
|
||||
// Pass empty string to avoid issues when running from different directories
|
||||
Resource::List::init(""); // Empty executable_path in release
|
||||
Resource::List::get()->loadFromString(assets_config, "", system_folder_); // Empty PREFIX for pack
|
||||
std::cout << "Asset system initialized from pack\n";
|
||||
|
||||
#else
|
||||
// ============================================================
|
||||
// DEVELOPMENT BUILD: Filesystem-first architecture
|
||||
// ============================================================
|
||||
std::cout << "\n** DEVELOPMENT MODE: Filesystem-first initialization\n";
|
||||
|
||||
// 1. Initialize Asset system from filesystem
|
||||
Resource::List::init(executable_path_);
|
||||
|
||||
// 2. Load asset configuration from disk
|
||||
// Note: Asset verification happens during Resource::Cache::load()
|
||||
setFileList();
|
||||
|
||||
// 3. Initialize resource pack system (optional, with fallback)
|
||||
std::cout << "Initializing resource pack (development mode): " << pack_path << '\n';
|
||||
Resource::Helper::initializeResourceSystem(pack_path, true);
|
||||
|
||||
#endif
|
||||
|
||||
// Configura la ruta y carga las opciones desde un fichero
|
||||
Options::setConfigFile(Resource::List::get()->get("config.yaml")); // NOLINT(readability-static-accessed-through-instance)
|
||||
Options::loadFromFile();
|
||||
|
||||
// Configura la ruta y carga los presets de PostFX
|
||||
Options::setPostFXFile(Resource::List::get()->get("postfx.yaml")); // NOLINT(readability-static-accessed-through-instance)
|
||||
Options::loadPostFXFromFile();
|
||||
|
||||
// Configura la ruta y carga los presets del shader CrtPi
|
||||
Options::setCrtPiFile(Resource::List::get()->get("crtpi.yaml")); // NOLINT(readability-static-accessed-through-instance)
|
||||
Options::loadCrtPiFromFile();
|
||||
|
||||
// En mode quiosc, forçar pantalla completa independentment de la configuració
|
||||
if (Options::kiosk.enabled) {
|
||||
Options::video.fullscreen = true;
|
||||
}
|
||||
|
||||
// Inicializa JailAudio
|
||||
Audio::init();
|
||||
|
||||
// Crea los objetos
|
||||
Screen::init();
|
||||
|
||||
// Initialize resources (works for both release and development)
|
||||
Resource::Cache::init();
|
||||
Notifier::init("", "8bithud");
|
||||
RenderInfo::init();
|
||||
Console::init("8bithud");
|
||||
Screen::get()->setNotificationsEnabled(true);
|
||||
|
||||
// Special handling for gamecontrollerdb.txt - SDL needs filesystem path
|
||||
#ifdef RELEASE_BUILD
|
||||
// In release, construct the path manually (not from Asset which has empty executable_path)
|
||||
std::string gamecontroller_db = executable_path_ + PREFIX + "/gamecontrollerdb.txt";
|
||||
Input::init(gamecontroller_db);
|
||||
#else
|
||||
// In development, use Asset as normal
|
||||
Input::init(Resource::List::get()->get("gamecontrollerdb.txt")); // NOLINT(readability-static-accessed-through-instance) Carga configuración de controles
|
||||
#endif
|
||||
|
||||
// Aplica las teclas y botones del gamepad configurados desde Options
|
||||
Input::get()->applyKeyboardBindingsFromOptions();
|
||||
Input::get()->applyGamepadBindingsFromOptions();
|
||||
|
||||
#ifdef _DEBUG
|
||||
Debug::init();
|
||||
Debug::get()->setDebugFile(Resource::List::get()->get("debug.yaml"));
|
||||
Debug::get()->loadFromFile();
|
||||
SceneManager::current = Debug::get()->getInitialScene();
|
||||
MapEditor::init();
|
||||
#endif
|
||||
|
||||
std::cout << "\n"; // Fin de inicialización de sistemas
|
||||
|
||||
// Inicializa el sistema de localización (antes de Cheevos que usa textos traducidos)
|
||||
#ifdef RELEASE_BUILD
|
||||
{
|
||||
// En release el locale está en el pack, no en el filesystem
|
||||
std::string locale_key = Resource::List::get()->get(Options::language + ".yaml"); // NOLINT(readability-static-accessed-through-instance)
|
||||
auto locale_bytes = Resource::Helper::loadFile(locale_key);
|
||||
std::string locale_content(locale_bytes.begin(), locale_bytes.end());
|
||||
Locale::initFromContent(locale_content);
|
||||
}
|
||||
#else
|
||||
Locale::init(Resource::List::get()->get(Options::language + ".yaml")); // NOLINT(readability-static-accessed-through-instance)
|
||||
#endif
|
||||
|
||||
// Special handling for cheevos.bin - also needs filesystem path
|
||||
#ifdef RELEASE_BUILD
|
||||
std::string cheevos_path = system_folder_ + "/cheevos.bin";
|
||||
Cheevos::init(cheevos_path);
|
||||
#else
|
||||
Cheevos::init(Resource::List::get()->get("cheevos.bin"));
|
||||
#endif
|
||||
}
|
||||
|
||||
Director::~Director() {
|
||||
// Guarda las opciones a un fichero
|
||||
Options::saveToFile();
|
||||
|
||||
// Destruye los singletones
|
||||
Cheevos::destroy();
|
||||
Locale::destroy();
|
||||
#ifdef _DEBUG
|
||||
MapEditor::destroy();
|
||||
Debug::destroy();
|
||||
#endif
|
||||
Input::destroy();
|
||||
Console::destroy();
|
||||
RenderInfo::destroy();
|
||||
Notifier::destroy();
|
||||
Resource::Cache::destroy();
|
||||
Resource::Helper::shutdownResourceSystem(); // Shutdown resource pack system
|
||||
Audio::destroy();
|
||||
Screen::destroy();
|
||||
Resource::List::destroy();
|
||||
|
||||
SDL_Quit();
|
||||
|
||||
std::cout << "\nBye!" << '\n';
|
||||
}
|
||||
|
||||
// Crea la carpeta del sistema donde guardar datos
|
||||
void Director::createSystemFolder(const std::string& folder) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
#ifdef _WIN32
|
||||
system_folder_ = std::string(getenv("APPDATA")) + "/" + folder;
|
||||
#elif __APPLE__
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
const char* homedir = pw->pw_dir;
|
||||
system_folder_ = std::string(homedir) + "/Library/Application Support" + "/" + folder;
|
||||
#elif __linux__
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
const char* homedir = pw->pw_dir;
|
||||
system_folder_ = std::string(homedir) + "/.config/" + folder;
|
||||
|
||||
{
|
||||
// Intenta crear ".config", per si no existeix
|
||||
std::string config_base_folder = std::string(homedir) + "/.config";
|
||||
int ret = mkdir(config_base_folder.c_str(), S_IRWXU);
|
||||
if (ret == -1 && errno != EEXIST) {
|
||||
printf("ERROR CREATING CONFIG BASE FOLDER.");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
struct stat st = {.st_dev = 0};
|
||||
if (stat(system_folder_.c_str(), &st) == -1) {
|
||||
errno = 0;
|
||||
#ifdef _WIN32
|
||||
int ret = mkdir(system_folder_.c_str());
|
||||
#else
|
||||
int ret = mkdir(system_folder_.c_str(), S_IRWXU);
|
||||
#endif
|
||||
|
||||
if (ret == -1) {
|
||||
switch (errno) {
|
||||
case EACCES:
|
||||
printf("the parent directory does not allow write");
|
||||
exit(EXIT_FAILURE);
|
||||
|
||||
case EEXIST:
|
||||
printf("pathname already exists");
|
||||
exit(EXIT_FAILURE);
|
||||
|
||||
case ENAMETOOLONG:
|
||||
printf("pathname is too long");
|
||||
exit(EXIT_FAILURE);
|
||||
|
||||
default:
|
||||
perror("mkdir");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Carga la configuración de assets desde assets.yaml
|
||||
void Director::setFileList() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Determinar el prefijo de ruta según la plataforma
|
||||
#ifdef MACOS_BUNDLE
|
||||
const std::string PREFIX = "/../Resources";
|
||||
#else
|
||||
const std::string PREFIX;
|
||||
#endif
|
||||
|
||||
// Construir ruta al archivo de configuración de assets
|
||||
std::string config_path = executable_path_ + PREFIX + "/config/assets.yaml";
|
||||
|
||||
// Cargar todos los assets desde el archivo de configuración
|
||||
// La verificación de existencia de archivos se realiza durante Resource::Cache::load()
|
||||
Resource::List::get()->loadFromFile(config_path, PREFIX, system_folder_);
|
||||
}
|
||||
|
||||
// Ejecuta la seccion de juego con el logo
|
||||
void Director::runLogo() {
|
||||
auto logo = std::make_unique<Logo>();
|
||||
logo->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion de juego de la pantalla de carga
|
||||
void Director::runLoadingScreen() {
|
||||
auto loading_screen = std::make_unique<LoadingScreen>();
|
||||
loading_screen->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion de juego con el titulo y los menus
|
||||
void Director::runTitle() {
|
||||
auto title = std::make_unique<Title>();
|
||||
title->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion de los creditos del juego
|
||||
void Director::runCredits() {
|
||||
auto credits = std::make_unique<Credits>();
|
||||
credits->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion de la demo, donde se ven pantallas del juego
|
||||
void Director::runDemo() {
|
||||
auto game = std::make_unique<Game>(Game::Mode::DEMO);
|
||||
game->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion del final del juego
|
||||
void Director::runEnding() {
|
||||
auto ending = std::make_unique<Ending>();
|
||||
ending->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion del final del juego
|
||||
void Director::runEnding2() {
|
||||
auto ending2 = std::make_unique<Ending2>();
|
||||
ending2->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion del final de la partida
|
||||
void Director::runGameOver() {
|
||||
auto game_over = std::make_unique<GameOver>();
|
||||
game_over->run();
|
||||
}
|
||||
|
||||
// Ejecuta la seccion de juego donde se juega
|
||||
void Director::runGame() {
|
||||
Audio::get()->stopMusic();
|
||||
auto game = std::make_unique<Game>(Game::Mode::GAME);
|
||||
game->run();
|
||||
}
|
||||
|
||||
auto Director::run() -> int {
|
||||
// Bucle principal
|
||||
while (SceneManager::current != SceneManager::Scene::QUIT) {
|
||||
const SceneManager::Scene ACTIVE = SceneManager::current;
|
||||
|
||||
switch (SceneManager::current) {
|
||||
case SceneManager::Scene::LOGO:
|
||||
runLogo();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::LOADING_SCREEN:
|
||||
runLoadingScreen();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::TITLE:
|
||||
runTitle();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::CREDITS:
|
||||
runCredits();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::DEMO:
|
||||
runDemo();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::GAME:
|
||||
runGame();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::GAME_OVER:
|
||||
runGameOver();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::ENDING:
|
||||
runEnding();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::ENDING2:
|
||||
runEnding2();
|
||||
break;
|
||||
|
||||
case SceneManager::Scene::RESTART_CURRENT:
|
||||
// La escena salió por RESTART_CURRENT → relanzar la escena guardada
|
||||
SceneManager::current = SceneManager::scene_before_restart;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Si la escena que acaba de correr dejó RESTART_CURRENT pendiente,
|
||||
// restaurar la escena que estaba activa para relanzarla en la próxima iteración
|
||||
if (SceneManager::current == SceneManager::Scene::RESTART_CURRENT) {
|
||||
SceneManager::current = ACTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
30
source/core/system/director.hpp
Normal file
30
source/core/system/director.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string> // Para string
|
||||
|
||||
class Director {
|
||||
public:
|
||||
Director(); // Constructor
|
||||
~Director(); // Destructor
|
||||
static auto run() -> int; // Bucle principal
|
||||
|
||||
private:
|
||||
// --- Variables ---
|
||||
std::string executable_path_; // Path del ejecutable
|
||||
std::string system_folder_; // Carpeta del sistema donde guardar datos
|
||||
|
||||
// --- Funciones ---
|
||||
void createSystemFolder(const std::string& folder); // Crea la carpeta del sistema donde guardar datos
|
||||
void setFileList(); // Carga la configuración de assets desde assets.yaml
|
||||
static void runLogo(); // Ejecuta la seccion de juego con el logo
|
||||
static void runLoadingScreen(); // Ejecuta la seccion de juego de la pantalla de carga
|
||||
static void runTitle(); // Ejecuta la seccion de juego con el titulo y los menus
|
||||
static void runCredits(); // Ejecuta la seccion de los creditos del juego
|
||||
static void runDemo(); // Ejecuta la seccion de la demo, donde se ven pantallas del juego
|
||||
static void runEnding(); // Ejecuta la seccion del final del juego
|
||||
static void runEnding2(); // Ejecuta la seccion del final del juego
|
||||
static void runGameOver(); // Ejecuta la seccion del final de la partida
|
||||
static void runGame(); // Ejecuta la seccion de juego donde se juega
|
||||
};
|
||||
31
source/core/system/global_events.cpp
Normal file
31
source/core/system/global_events.cpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#include "core/system/global_events.hpp"
|
||||
|
||||
#include "core/input/mouse.hpp"
|
||||
#include "game/options.hpp" // Para Options, options, OptionsGame, OptionsAudio
|
||||
#include "game/scene_manager.hpp" // Para SceneManager
|
||||
#include "game/ui/console.hpp" // Para Console
|
||||
|
||||
namespace GlobalEvents {
|
||||
// Comprueba los eventos que se pueden producir en cualquier sección del juego
|
||||
void handle(const SDL_Event& event) {
|
||||
// Evento de salida de la aplicación
|
||||
if (event.type == SDL_EVENT_QUIT) {
|
||||
SceneManager::current = SceneManager::Scene::QUIT;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_RENDER_DEVICE_RESET || event.type == SDL_EVENT_RENDER_TARGETS_RESET) {
|
||||
// reLoadTextures();
|
||||
}
|
||||
|
||||
// Enrutar eventos de texto a la consola cuando está activa
|
||||
if (Console::get() != nullptr && Console::get()->isActive()) {
|
||||
if (event.type == SDL_EVENT_TEXT_INPUT || event.type == SDL_EVENT_KEY_DOWN) {
|
||||
Console::get()->handleEvent(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Mouse::handleEvent(event);
|
||||
}
|
||||
} // namespace GlobalEvents
|
||||
8
source/core/system/global_events.hpp
Normal file
8
source/core/system/global_events.hpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace GlobalEvents {
|
||||
// Comprueba los eventos que se pueden producir en cualquier sección del juego
|
||||
void handle(const SDL_Event& event);
|
||||
} // namespace GlobalEvents
|
||||
2
source/external/.clang-format
vendored
Normal file
2
source/external/.clang-format
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
DisableFormat: true
|
||||
SortIncludes: Never
|
||||
4
source/external/.clang-tidy
vendored
Normal file
4
source/external/.clang-tidy
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# source/external/.clang-tidy
|
||||
Checks: '-*'
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: ''
|
||||
14726
source/external/fkyaml_node.hpp
vendored
Normal file
14726
source/external/fkyaml_node.hpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9251
source/external/stb_image.h
vendored
Normal file
9251
source/external/stb_image.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
5565
source/external/stb_vorbis.h
vendored
Normal file
5565
source/external/stb_vorbis.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
106
source/game/defaults.hpp
Normal file
106
source/game/defaults.hpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen::Filter
|
||||
#include "utils/defines.hpp" // Para GameCanvas
|
||||
#include "utils/utils.hpp" // Para PaletteColor
|
||||
|
||||
// Forward declarations from Options namespace
|
||||
namespace Options {
|
||||
// enum class ControlScheme;
|
||||
enum class NotificationPosition;
|
||||
} // namespace Options
|
||||
|
||||
namespace Defaults::Canvas {
|
||||
constexpr int WIDTH = GameCanvas::WIDTH; // Ancho del canvas del juego (256)
|
||||
constexpr int HEIGHT = GameCanvas::HEIGHT; // Alto del canvas del juego (192)
|
||||
} // namespace Defaults::Canvas
|
||||
|
||||
namespace Defaults::Window {
|
||||
constexpr int ZOOM = 2; // Zoom de la ventana por defecto
|
||||
} // namespace Defaults::Window
|
||||
|
||||
namespace Defaults::Video {
|
||||
constexpr bool FULLSCREEN = false; // Modo de pantalla completa por defecto (false = ventana)
|
||||
constexpr Screen::Filter FILTER = Screen::Filter::NEAREST; // Filtro por defecto
|
||||
constexpr bool VERTICAL_SYNC = true; // Vsync activado por defecto
|
||||
constexpr bool SHADER_ENABLED = false; // Shaders de post-procesado desactivados por defecto
|
||||
constexpr bool SUPERSAMPLING = false; // Supersampling desactivado por defecto
|
||||
constexpr bool INTEGER_SCALE = true; // Escalado entero activado por defecto
|
||||
constexpr bool KEEP_ASPECT = true; // Mantener aspecto activado por defecto
|
||||
constexpr const char* PALETTE_NAME = "cpc"; // Paleta por defecto
|
||||
constexpr const char* PALETTE_SORT = "original"; // Modo de ordenación de paleta por defecto
|
||||
constexpr bool LINEAR_UPSCALE = false; // Upscale NEAREST por defecto
|
||||
constexpr int DOWNSCALE_ALGO = 1; // Downscale Lanczos2 por defecto
|
||||
constexpr bool GPU_ACCELERATION = true; // Aceleración GPU activada por defecto
|
||||
} // namespace Defaults::Video
|
||||
|
||||
namespace Defaults::Border {
|
||||
constexpr bool ENABLED = true; // Borde activado por defecto
|
||||
constexpr int WIDTH = 32; // Ancho del borde por defecto
|
||||
constexpr int HEIGHT = 24; // Alto del borde por defectoF
|
||||
} // namespace Defaults::Border
|
||||
|
||||
namespace Defaults::Audio {
|
||||
constexpr float VOLUME = 1.0F; // Volumen por defecto
|
||||
constexpr bool ENABLED = true; // Audio por defecto
|
||||
} // namespace Defaults::Audio
|
||||
|
||||
namespace Defaults::Music {
|
||||
constexpr float VOLUME = 0.8F; // Volumen por defecto de la musica
|
||||
constexpr bool ENABLED = true; // Musica habilitada por defecto
|
||||
} // namespace Defaults::Music
|
||||
|
||||
namespace Defaults::Sound {
|
||||
constexpr float VOLUME = 1.0F; // Volumen por defecto de los efectos de sonido
|
||||
constexpr bool ENABLED = true; // Sonido habilitado por defecto
|
||||
} // namespace Defaults::Sound
|
||||
|
||||
namespace Defaults::Cheat {
|
||||
constexpr bool INFINITE_LIVES = false; // Vidas infinitas desactivadas por defecto
|
||||
constexpr bool INVINCIBLE = false; // Invencibilidad desactivada por defecto
|
||||
constexpr bool JAIL_IS_OPEN = false; // Jail abierta desactivada por defecto
|
||||
} // namespace Defaults::Cheat
|
||||
|
||||
namespace Defaults::Stats {
|
||||
constexpr int ROOMS = 0; // Habitaciones visitadas por defecto
|
||||
constexpr int ITEMS = 0; // Items obtenidos por defecto
|
||||
} // namespace Defaults::Stats
|
||||
|
||||
namespace Defaults::Controls {
|
||||
constexpr SDL_Scancode KEY_LEFT = SDL_SCANCODE_LEFT; // Tecla izquierda por defecto
|
||||
constexpr SDL_Scancode KEY_RIGHT = SDL_SCANCODE_RIGHT; // Tecla derecha por defecto
|
||||
constexpr SDL_Scancode KEY_JUMP = SDL_SCANCODE_UP; // Tecla salto por defecto
|
||||
|
||||
constexpr int GAMEPAD_BUTTON_LEFT = SDL_GAMEPAD_BUTTON_DPAD_LEFT; // Botón izquierda por defecto
|
||||
constexpr int GAMEPAD_BUTTON_RIGHT = SDL_GAMEPAD_BUTTON_DPAD_RIGHT; // Botón derecha por defecto
|
||||
constexpr int GAMEPAD_BUTTON_JUMP = SDL_GAMEPAD_BUTTON_WEST; // Botón salto por defecto
|
||||
} // namespace Defaults::Controls
|
||||
|
||||
namespace Defaults::Kiosk {
|
||||
constexpr bool ENABLED = false; // Modo kiosko desactivado por defecto
|
||||
constexpr const char* TEXT = "KIOSK MODE"; // Texto del modo kiosko por defecto
|
||||
constexpr bool INFINITE_LIVES = true; // Vidas infinitas en modo kiosko desactivadas por defecto
|
||||
} // namespace Defaults::Kiosk
|
||||
|
||||
namespace Defaults::Localization {
|
||||
constexpr const char* LANGUAGE = "ca"; // Idioma por defecto (en = inglés, ca = catalán)
|
||||
} // namespace Defaults::Localization
|
||||
|
||||
namespace Defaults::Game::Items {
|
||||
constexpr const float PERCENT_TO_OPEN_THE_JAIL = 0.9F; // Porcentaje de items necesarios para abrir la jail
|
||||
} // namespace Defaults::Game::Items
|
||||
|
||||
namespace Defaults::Game::Room {
|
||||
constexpr const char* INITIAL = "03.yaml"; // Habitación de inicio
|
||||
constexpr const char* END_ROOM = "01"; // Habitación final (jail)
|
||||
} // namespace Defaults::Game::Room
|
||||
|
||||
namespace Defaults::Game::Player {
|
||||
constexpr int SPAWN_X = 25 * Tile::SIZE; // Posición X inicial
|
||||
constexpr int SPAWN_Y = 13 * Tile::SIZE; // Posición Y inicial
|
||||
constexpr SDL_FlipMode SPAWN_FLIP = Flip::LEFT; // Orientación inicial
|
||||
constexpr const char* SKIN = "default"; // Skin del jugador por defecto
|
||||
constexpr int COLOR = -1; // Color del jugador (-1 = automático según cheats)
|
||||
} // namespace Defaults::Game::Player
|
||||
89
source/game/editor/editor_statusbar.cpp
Normal file
89
source/game/editor/editor_statusbar.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include "game/editor/editor_statusbar.hpp"
|
||||
|
||||
#include <string> // Para to_string
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource::Cache
|
||||
#include "game/options.hpp" // Para Options::game
|
||||
#include "utils/defines.hpp" // Para Tile::SIZE
|
||||
#include "utils/utils.hpp" // Para stringToColor, toLower
|
||||
|
||||
// Constructor
|
||||
EditorStatusBar::EditorStatusBar(std::string room_number, std::string room_name)
|
||||
: room_number_(std::move(room_number)),
|
||||
room_name_(std::move(room_name)) {
|
||||
const float SURFACE_WIDTH = Options::game.width;
|
||||
constexpr float SURFACE_HEIGHT = 6.0F * Tile::SIZE; // 48 pixels, igual que el scoreboard
|
||||
|
||||
surface_ = std::make_shared<Surface>(SURFACE_WIDTH, SURFACE_HEIGHT);
|
||||
surface_dest_ = {.x = 0, .y = Options::game.height - SURFACE_HEIGHT, .w = SURFACE_WIDTH, .h = SURFACE_HEIGHT};
|
||||
}
|
||||
|
||||
// Pinta la barra de estado en pantalla
|
||||
void EditorStatusBar::render() {
|
||||
surface_->render(nullptr, &surface_dest_);
|
||||
}
|
||||
|
||||
// Actualiza la barra de estado
|
||||
void EditorStatusBar::update([[maybe_unused]] float delta_time) {
|
||||
fillTexture();
|
||||
}
|
||||
|
||||
void EditorStatusBar::setMouseTile(int tile_x, int tile_y) {
|
||||
mouse_tile_x_ = tile_x;
|
||||
mouse_tile_y_ = tile_y;
|
||||
}
|
||||
|
||||
void EditorStatusBar::setLine2(const std::string& text) { line2_ = text; }
|
||||
void EditorStatusBar::setLine3(const std::string& text) { line3_ = text; }
|
||||
void EditorStatusBar::setLine4(const std::string& text) { line4_ = text; }
|
||||
void EditorStatusBar::setLine5(const std::string& text) { line5_ = text; }
|
||||
|
||||
// Dibuja los elementos en la surface
|
||||
void EditorStatusBar::fillTexture() {
|
||||
auto previous_renderer = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(surface_);
|
||||
|
||||
surface_->clear(stringToColor("black"));
|
||||
|
||||
auto text = Resource::Cache::get()->getText("8bithud");
|
||||
const Uint8 LABEL_COLOR = stringToColor("bright_cyan");
|
||||
const Uint8 VALUE_COLOR = stringToColor("white");
|
||||
const Uint8 DETAIL_COLOR = stringToColor("bright_yellow");
|
||||
|
||||
// Línea 1: Nombre de la habitación
|
||||
text->writeColored(LEFT_X, LINE1_Y, toLower(room_number_ + " " + room_name_), LABEL_COLOR);
|
||||
|
||||
// Línea 2: Propiedades de room o info de enemigo
|
||||
if (!line2_.empty()) {
|
||||
text->writeColored(LEFT_X, LINE2_Y, toLower(line2_), DETAIL_COLOR);
|
||||
}
|
||||
|
||||
// Línea 3: Conexiones+items o propiedades del enemigo
|
||||
if (!line3_.empty()) {
|
||||
text->writeColored(LEFT_X, LINE3_Y, toLower(line3_), VALUE_COLOR);
|
||||
}
|
||||
|
||||
// Línea 4: Extra
|
||||
if (!line4_.empty()) {
|
||||
text->writeColored(LEFT_X, LINE4_Y, toLower(line4_), DETAIL_COLOR);
|
||||
}
|
||||
|
||||
// Línea 5: Tile coords + drag info
|
||||
const std::string TILE_X_STR = (mouse_tile_x_ < 10 ? "0" : "") + std::to_string(mouse_tile_x_);
|
||||
const std::string TILE_Y_STR = (mouse_tile_y_ < 10 ? "0" : "") + std::to_string(mouse_tile_y_);
|
||||
std::string line5 = "tile:" + TILE_X_STR + "," + TILE_Y_STR;
|
||||
if (!line5_.empty()) {
|
||||
line5 += " " + line5_;
|
||||
}
|
||||
text->writeColored(LEFT_X, LINE5_Y, toLower(line5), stringToColor("bright_green"));
|
||||
|
||||
Screen::get()->setRendererSurface(previous_renderer);
|
||||
}
|
||||
|
||||
#endif // _DEBUG
|
||||
52
source/game/editor/editor_statusbar.hpp
Normal file
52
source/game/editor/editor_statusbar.hpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
|
||||
class Surface;
|
||||
|
||||
class EditorStatusBar {
|
||||
public:
|
||||
EditorStatusBar(std::string room_number, std::string room_name);
|
||||
~EditorStatusBar() = default;
|
||||
|
||||
void render();
|
||||
void update(float delta_time);
|
||||
void setMouseTile(int tile_x, int tile_y);
|
||||
void setLine2(const std::string& text);
|
||||
void setLine3(const std::string& text);
|
||||
void setLine4(const std::string& text);
|
||||
void setLine5(const std::string& text);
|
||||
|
||||
private:
|
||||
void fillTexture(); // Dibuja los elementos en la surface
|
||||
|
||||
// Constantes de posición (en pixels dentro de la surface de 256x48)
|
||||
// Font 8bithud lowercase = 6px alto → 5 líneas con 8px de separación
|
||||
static constexpr int LINE1_Y = 2; // Nombre de la habitación
|
||||
static constexpr int LINE2_Y = 10; // Propiedades de room / enemy info
|
||||
static constexpr int LINE3_Y = 18; // Conexiones+items / enemy detail
|
||||
static constexpr int LINE4_Y = 26; // Extra
|
||||
static constexpr int LINE5_Y = 34; // Tile coords + drag info
|
||||
static constexpr int LEFT_X = 4; // Margen izquierdo
|
||||
|
||||
// Objetos
|
||||
std::shared_ptr<Surface> surface_; // Surface donde dibujar la barra
|
||||
SDL_FRect surface_dest_{}; // Rectángulo destino en pantalla
|
||||
|
||||
// Variables
|
||||
std::string room_number_; // Número de la habitación
|
||||
std::string room_name_; // Nombre de la habitación
|
||||
int mouse_tile_x_{0}; // Coordenada X del ratón en tiles
|
||||
int mouse_tile_y_{0}; // Coordenada Y del ratón en tiles
|
||||
std::string line2_; // Contenido de la línea 2
|
||||
std::string line3_; // Contenido de la línea 3
|
||||
std::string line4_; // Contenido de la línea 4
|
||||
std::string line5_; // Contenido de la línea 5
|
||||
};
|
||||
|
||||
#endif // _DEBUG
|
||||
1633
source/game/editor/map_editor.cpp
Normal file
1633
source/game/editor/map_editor.cpp
Normal file
File diff suppressed because it is too large
Load Diff
160
source/game/editor/map_editor.hpp
Normal file
160
source/game/editor/map_editor.hpp
Normal file
@@ -0,0 +1,160 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr, unique_ptr
|
||||
#include <string> // Para string
|
||||
|
||||
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||
#include "game/editor/mini_map.hpp" // Para MiniMap
|
||||
#include "game/editor/tile_picker.hpp" // Para TilePicker
|
||||
#include "game/entities/enemy.hpp" // Para Enemy::Data
|
||||
#include "game/entities/item.hpp" // Para Item::Data
|
||||
#include "game/entities/player.hpp" // Para Player::SpawnData
|
||||
#include "game/gameplay/room.hpp" // Para Room::Data
|
||||
#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data
|
||||
#include "game/options.hpp" // Para Options::Cheat
|
||||
|
||||
class EditorStatusBar;
|
||||
|
||||
class MapEditor {
|
||||
public:
|
||||
static void init(); // [SINGLETON] Crea el objeto
|
||||
static void destroy(); // [SINGLETON] Destruye el objeto
|
||||
static auto get() -> MapEditor*; // [SINGLETON] Obtiene el objeto
|
||||
|
||||
void enter(std::shared_ptr<Room> room, std::shared_ptr<Player> player, const std::string& room_path, std::shared_ptr<Scoreboard::Data> scoreboard_data);
|
||||
void exit();
|
||||
[[nodiscard]] auto isActive() const -> bool { return active_; }
|
||||
|
||||
void update(float delta_time);
|
||||
void render();
|
||||
void handleEvent(const SDL_Event& event);
|
||||
auto revert() -> std::string;
|
||||
|
||||
// Comandos para enemigos (llamados desde console_commands)
|
||||
auto setEnemyProperty(const std::string& property, const std::string& value) -> std::string;
|
||||
auto addEnemy() -> std::string;
|
||||
auto deleteEnemy() -> std::string;
|
||||
auto duplicateEnemy() -> std::string;
|
||||
[[nodiscard]] auto hasSelectedEnemy() const -> bool;
|
||||
[[nodiscard]] auto getSetCompletions() const -> std::vector<std::string>;
|
||||
|
||||
// Comandos para propiedades de la habitación
|
||||
auto setRoomProperty(const std::string& property, const std::string& value) -> std::string;
|
||||
auto createNewRoom(const std::string& direction = "") -> std::string;
|
||||
auto deleteRoom() -> std::string;
|
||||
|
||||
// Opciones del editor (llamados desde console_commands / teclas)
|
||||
auto showInfo(bool show) -> std::string;
|
||||
auto showGrid(bool show) -> std::string;
|
||||
[[nodiscard]] auto isGridEnabled() const -> bool { return settings_.grid; }
|
||||
void toggleMiniMap();
|
||||
void setReenter(bool value) { reenter_ = value; }
|
||||
auto setMiniMapBg(const std::string& color) -> std::string;
|
||||
auto setMiniMapConn(const std::string& color) -> std::string;
|
||||
|
||||
// Comandos para items
|
||||
auto setItemProperty(const std::string& property, const std::string& value) -> std::string;
|
||||
auto addItem() -> std::string;
|
||||
auto deleteItem() -> std::string;
|
||||
auto duplicateItem() -> std::string;
|
||||
[[nodiscard]] auto hasSelectedItem() const -> bool;
|
||||
void openTilePicker(const std::string& tileset_name, int current_tile);
|
||||
|
||||
private:
|
||||
static MapEditor* instance_; // NOLINT(readability-identifier-naming) [SINGLETON] Objeto privado
|
||||
|
||||
MapEditor(); // Constructor
|
||||
~MapEditor(); // Destructor
|
||||
|
||||
// Opciones persistentes del editor
|
||||
struct Settings {
|
||||
bool grid{false};
|
||||
bool show_render_info{false};
|
||||
std::string minimap_bg{"blue"};
|
||||
std::string minimap_conn{"white"};
|
||||
};
|
||||
Settings settings_;
|
||||
void loadSettings();
|
||||
void saveSettings() const;
|
||||
|
||||
// Tipos para drag & drop y selección
|
||||
enum class DragTarget { NONE,
|
||||
PLAYER,
|
||||
ENEMY_INITIAL,
|
||||
ENEMY_BOUND1,
|
||||
ENEMY_BOUND2,
|
||||
ITEM };
|
||||
|
||||
struct DragState {
|
||||
DragTarget target{DragTarget::NONE};
|
||||
int index{-1};
|
||||
float offset_x{0.0F};
|
||||
float offset_y{0.0F};
|
||||
float snap_x{0.0F};
|
||||
float snap_y{0.0F};
|
||||
bool moved{false}; // true si el ratón se movió durante el drag
|
||||
};
|
||||
|
||||
// Métodos internos
|
||||
void updateMousePosition();
|
||||
void renderEnemyBoundaries();
|
||||
static void renderBoundaryMarker(float x, float y, Uint8 color);
|
||||
void renderSelectionHighlight();
|
||||
void renderGrid() const;
|
||||
void handleMouseDown(float game_x, float game_y);
|
||||
void handleMouseUp();
|
||||
void updateDrag();
|
||||
void autosave();
|
||||
void updateStatusBarInfo();
|
||||
static auto snapToGrid(float value) -> float;
|
||||
static auto pointInRect(float px, float py, const SDL_FRect& rect) -> bool;
|
||||
|
||||
// Estado del editor
|
||||
bool active_{false};
|
||||
DragState drag_;
|
||||
int selected_enemy_{-1}; // Índice del enemigo seleccionado (-1 = ninguno)
|
||||
int selected_item_{-1}; // Índice del item seleccionado (-1 = ninguno)
|
||||
static constexpr int NO_BRUSH = -2; // Sin brush activo
|
||||
static constexpr int ERASER_BRUSH = -1; // Brush borrador (pinta tile vacío = -1)
|
||||
int brush_tile_{NO_BRUSH}; // Tile activo para pintar
|
||||
bool painting_{false}; // true mientras se está pintando con click izquierdo mantenido
|
||||
|
||||
// Datos de la habitación
|
||||
Room::Data room_data_;
|
||||
std::string room_path_;
|
||||
std::string file_path_;
|
||||
|
||||
// YAML: nodo original (para campos que no se editan: name_ca, etc.)
|
||||
fkyaml::node yaml_;
|
||||
fkyaml::node yaml_backup_;
|
||||
|
||||
// Referencias a objetos vivos
|
||||
std::shared_ptr<Room> room_;
|
||||
std::shared_ptr<Player> player_;
|
||||
std::shared_ptr<Scoreboard::Data> scoreboard_data_;
|
||||
|
||||
// Barra de estado del editor
|
||||
std::unique_ptr<EditorStatusBar> statusbar_;
|
||||
|
||||
// Tile picker y mini mapa
|
||||
TilePicker tile_picker_;
|
||||
std::unique_ptr<MiniMap> mini_map_;
|
||||
bool mini_map_visible_{false};
|
||||
|
||||
// Estado del ratón
|
||||
float mouse_game_x_{0.0F};
|
||||
float mouse_game_y_{0.0F};
|
||||
int mouse_tile_x_{0};
|
||||
int mouse_tile_y_{0};
|
||||
|
||||
// Estado previo (para restaurar al salir)
|
||||
Options::Cheat::State invincible_before_editor_{Options::Cheat::State::DISABLED};
|
||||
bool render_info_before_editor_{false};
|
||||
bool reenter_{false}; // true cuando es un re-enter tras cambio de room (no tocar render_info)
|
||||
};
|
||||
|
||||
#endif // _DEBUG
|
||||
391
source/game/editor/mini_map.cpp
Normal file
391
source/game/editor/mini_map.cpp
Normal file
@@ -0,0 +1,391 @@
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include "game/editor/mini_map.hpp"
|
||||
|
||||
#include <algorithm> // Para std::max, std::min
|
||||
#include <array> // Para std::array
|
||||
#include <cmath> // Para std::floor
|
||||
#include <iostream> // Para cout
|
||||
#include <map> // Para std::map
|
||||
#include <queue> // Para queue (BFS)
|
||||
#include <set> // Para set
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource::Cache
|
||||
#include "game/gameplay/room.hpp" // Para Room::Data
|
||||
#include "utils/defines.hpp" // Para Tile::SIZE, PlayArea
|
||||
#include "utils/utils.hpp" // Para stringToColor
|
||||
|
||||
// Constructor: construye todo el minimapa
|
||||
MiniMap::MiniMap(Uint8 bg_color, Uint8 conn_color)
|
||||
: bg_color_(bg_color),
|
||||
conn_color_(conn_color) {
|
||||
buildTileColorTable("standard.gif");
|
||||
layoutRooms();
|
||||
buildRoomSurfaces();
|
||||
composeFinalSurface();
|
||||
}
|
||||
|
||||
// Regenera la surface final con nuevo color de fondo
|
||||
void MiniMap::rebuild(Uint8 bg_color, Uint8 conn_color) {
|
||||
bg_color_ = bg_color;
|
||||
conn_color_ = conn_color;
|
||||
composeFinalSurface();
|
||||
}
|
||||
|
||||
// Analiza el tileset y crea tabla: tile_index → color predominante
|
||||
void MiniMap::buildTileColorTable(const std::string& tileset_name) {
|
||||
auto tileset = Resource::Cache::get()->getSurface(tileset_name);
|
||||
if (!tileset) { return; }
|
||||
|
||||
tileset_width_ = static_cast<int>(tileset->getWidth()) / Tile::SIZE;
|
||||
tileset_transparent_ = tileset->getTransparentColor();
|
||||
int tileset_height = static_cast<int>(tileset->getHeight()) / Tile::SIZE;
|
||||
int total_tiles = tileset_width_ * tileset_height;
|
||||
|
||||
tile_colors_.resize(total_tiles, 0);
|
||||
|
||||
for (int tile = 0; tile < total_tiles; ++tile) {
|
||||
int tile_x = (tile % tileset_width_) * Tile::SIZE;
|
||||
int tile_y = (tile / tileset_width_) * Tile::SIZE;
|
||||
|
||||
// Contar frecuencia de cada color en el tile (ignorar el color transparente del tileset)
|
||||
Uint8 transparent = tileset->getTransparentColor();
|
||||
std::array<int, 256> freq{};
|
||||
for (int y = 0; y < Tile::SIZE; ++y) {
|
||||
for (int x = 0; x < Tile::SIZE; ++x) {
|
||||
Uint8 pixel = tileset->getPixel(tile_x + x, tile_y + y);
|
||||
if (pixel != transparent) {
|
||||
freq[pixel]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Encontrar el color más frecuente (transparent = tile vacío, no se pinta)
|
||||
Uint8 best_color = transparent;
|
||||
int best_count = 0;
|
||||
for (int c = 0; c < 256; ++c) {
|
||||
if (c == transparent) { continue; }
|
||||
if (freq[c] > best_count) {
|
||||
best_count = freq[c];
|
||||
best_color = static_cast<Uint8>(c);
|
||||
}
|
||||
}
|
||||
|
||||
tile_colors_[tile] = best_color;
|
||||
}
|
||||
}
|
||||
|
||||
// Posiciona las rooms en un grid usando BFS desde las conexiones
|
||||
void MiniMap::layoutRooms() {
|
||||
auto& rooms = Resource::Cache::get()->getRooms();
|
||||
if (rooms.empty()) { return; }
|
||||
|
||||
// Mapa de nombre → Room::Data
|
||||
std::unordered_map<std::string, std::shared_ptr<Room::Data>> room_map;
|
||||
for (const auto& r : rooms) {
|
||||
room_map[r.name] = r.room;
|
||||
}
|
||||
|
||||
// BFS para posicionar rooms
|
||||
std::set<std::string> visited;
|
||||
std::queue<std::pair<std::string, GridPos>> bfs;
|
||||
|
||||
// Empezar por la primera room
|
||||
const std::string& start = rooms[0].name;
|
||||
bfs.push({start, {.x = 0, .y = 0}});
|
||||
visited.insert(start);
|
||||
|
||||
// Grid ocupado: posición → nombre de room
|
||||
std::map<std::pair<int, int>, std::string> grid_occupied;
|
||||
|
||||
while (!bfs.empty()) {
|
||||
auto [name, pos] = bfs.front();
|
||||
bfs.pop();
|
||||
|
||||
auto key = std::make_pair(pos.x, pos.y);
|
||||
if (grid_occupied.contains(key)) { continue; }
|
||||
|
||||
grid_occupied[key] = name;
|
||||
room_positions_[name] = RoomMini{.surface = nullptr, .pos = pos};
|
||||
|
||||
auto it = room_map.find(name);
|
||||
if (it == room_map.end()) { continue; }
|
||||
const auto& data = it->second;
|
||||
|
||||
// Vecinos: up, down, left, right
|
||||
struct Neighbor {
|
||||
std::string room;
|
||||
int dx, dy;
|
||||
};
|
||||
std::array<Neighbor, 4> neighbors = {{
|
||||
{.room = data->upper_room, .dx = 0, .dy = -1},
|
||||
{.room = data->lower_room, .dx = 0, .dy = 1},
|
||||
{.room = data->left_room, .dx = -1, .dy = 0},
|
||||
{.room = data->right_room, .dx = 1, .dy = 0},
|
||||
}};
|
||||
|
||||
for (const auto& [neighbor_name, dx, dy] : neighbors) {
|
||||
if (neighbor_name == "0" || neighbor_name.empty()) { continue; }
|
||||
if (visited.contains(neighbor_name)) { continue; }
|
||||
|
||||
GridPos neighbor_pos = {.x = pos.x + dx, .y = pos.y + dy};
|
||||
auto nkey = std::make_pair(neighbor_pos.x, neighbor_pos.y);
|
||||
if (!grid_occupied.contains(nkey)) {
|
||||
visited.insert(neighbor_name);
|
||||
bfs.emplace(neighbor_name, neighbor_pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calcular bounds del grid
|
||||
min_grid_x_ = 0;
|
||||
min_grid_y_ = 0;
|
||||
int max_grid_x = 0;
|
||||
int max_grid_y = 0;
|
||||
for (const auto& [name, mini] : room_positions_) {
|
||||
min_grid_x_ = std::min(min_grid_x_, mini.pos.x);
|
||||
min_grid_y_ = std::min(min_grid_y_, mini.pos.y);
|
||||
max_grid_x = std::max(max_grid_x, mini.pos.x);
|
||||
max_grid_y = std::max(max_grid_y, mini.pos.y);
|
||||
}
|
||||
|
||||
int cols = max_grid_x - min_grid_x_ + 1;
|
||||
int rows = max_grid_y - min_grid_y_ + 1;
|
||||
map_width_ = cols * (CELL_W + GAP) - GAP + PADDING * 2;
|
||||
map_height_ = rows * (CELL_H + GAP) - GAP + PADDING * 2;
|
||||
|
||||
std::cout << "MiniMap: " << room_positions_.size() << " rooms, grid " << cols << "x" << rows
|
||||
<< " → " << map_width_ << "x" << map_height_ << " px\n";
|
||||
}
|
||||
|
||||
// Genera una mini-surface de 32x16 por room
|
||||
void MiniMap::buildRoomSurfaces() {
|
||||
for (auto& [name, mini] : room_positions_) {
|
||||
mini.surface = getRoomMiniSurface(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Genera la mini-surface de una room: 1 pixel por tile, color predominante
|
||||
auto MiniMap::getRoomMiniSurface(const std::string& room_name) -> std::shared_ptr<Surface> {
|
||||
auto room_data = Resource::Cache::get()->getRoom(room_name);
|
||||
if (!room_data) { return nullptr; }
|
||||
|
||||
auto surface = std::make_shared<Surface>(ROOM_W, ROOM_H);
|
||||
|
||||
auto prev = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(surface);
|
||||
surface->clear(stringToColor(room_data->bg_color));
|
||||
|
||||
const auto& tile_map = room_data->tile_map;
|
||||
for (int y = 0; y < ROOM_H; ++y) {
|
||||
for (int x = 0; x < ROOM_W; ++x) {
|
||||
int index = (y * ROOM_W) + x;
|
||||
if (index >= static_cast<int>(tile_map.size())) { continue; }
|
||||
|
||||
int tile = tile_map[index];
|
||||
if (tile < 0 || tile >= static_cast<int>(tile_colors_.size())) { continue; }
|
||||
|
||||
Uint8 color = tile_colors_[tile];
|
||||
if (color != tileset_transparent_) {
|
||||
surface->putPixel(x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Screen::get()->setRendererSurface(prev);
|
||||
return surface;
|
||||
}
|
||||
|
||||
// Compone la surface final con todas las rooms posicionadas
|
||||
void MiniMap::composeFinalSurface() {
|
||||
if (map_width_ <= 0 || map_height_ <= 0) { return; }
|
||||
|
||||
// Surface un poco más grande para la sombra del borde inferior/derecho
|
||||
map_surface_ = std::make_shared<Surface>(map_width_ + SHADOW_OFFSET, map_height_ + SHADOW_OFFSET);
|
||||
|
||||
auto prev = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(map_surface_);
|
||||
|
||||
// 1. Fondo general
|
||||
map_surface_->clear(bg_color_);
|
||||
|
||||
// 2. Líneas de conexión entre rooms (debajo de todo)
|
||||
drawConnections();
|
||||
|
||||
// 3. Sombras de las rooms (desplazadas 1px abajo-derecha)
|
||||
for (const auto& [name, mini] : room_positions_) {
|
||||
if (!mini.surface) { continue; }
|
||||
int px = cellPixelX(mini.pos.x) + SHADOW_OFFSET;
|
||||
int py = cellPixelY(mini.pos.y) + SHADOW_OFFSET;
|
||||
SDL_FRect shadow = {.x = static_cast<float>(px), .y = static_cast<float>(py), .w = static_cast<float>(CELL_W), .h = static_cast<float>(CELL_H)};
|
||||
map_surface_->fillRect(&shadow, COLOR_SHADOW);
|
||||
}
|
||||
|
||||
// 4. Borde negro de cada room + contenido de la room
|
||||
for (const auto& [name, mini] : room_positions_) {
|
||||
if (!mini.surface) { continue; }
|
||||
int px = cellPixelX(mini.pos.x);
|
||||
int py = cellPixelY(mini.pos.y);
|
||||
|
||||
// Borde negro (la celda entera)
|
||||
SDL_FRect cell = {.x = static_cast<float>(px), .y = static_cast<float>(py), .w = static_cast<float>(CELL_W), .h = static_cast<float>(CELL_H)};
|
||||
map_surface_->fillRect(&cell, COLOR_ROOM_BORDER);
|
||||
|
||||
// Miniroom dentro del borde
|
||||
SDL_FRect dst = {.x = static_cast<float>(px + BORDER), .y = static_cast<float>(py + BORDER), .w = static_cast<float>(ROOM_W), .h = static_cast<float>(ROOM_H)};
|
||||
mini.surface->render(nullptr, &dst);
|
||||
}
|
||||
|
||||
Screen::get()->setRendererSurface(prev);
|
||||
}
|
||||
|
||||
// Dibuja las líneas de conexión entre rooms vecinas
|
||||
void MiniMap::drawConnections() {
|
||||
for (const auto& [name, mini] : room_positions_) {
|
||||
auto room_data = Resource::Cache::get()->getRoom(name);
|
||||
if (!room_data) { continue; }
|
||||
|
||||
int px = cellPixelX(mini.pos.x);
|
||||
int py = cellPixelY(mini.pos.y);
|
||||
|
||||
// Conexión derecha
|
||||
if (room_data->right_room != "0" && !room_data->right_room.empty() && room_positions_.contains(room_data->right_room)) {
|
||||
int x1 = px + CELL_W;
|
||||
int y_mid = py + (CELL_H / 2) - 1;
|
||||
SDL_FRect line = {.x = static_cast<float>(x1), .y = static_cast<float>(y_mid), .w = static_cast<float>(GAP), .h = 3.0F};
|
||||
map_surface_->fillRect(&line, conn_color_);
|
||||
}
|
||||
|
||||
// Conexión abajo
|
||||
if (room_data->lower_room != "0" && !room_data->lower_room.empty() && room_positions_.contains(room_data->lower_room)) {
|
||||
int x_mid = px + (CELL_W / 2) - 1;
|
||||
int y1 = py + CELL_H;
|
||||
SDL_FRect line = {.x = static_cast<float>(x_mid), .y = static_cast<float>(y1), .w = 3.0F, .h = static_cast<float>(GAP)};
|
||||
map_surface_->fillRect(&line, conn_color_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Centra el viewport en una room
|
||||
void MiniMap::centerOnRoom(const std::string& room_name) {
|
||||
auto it = room_positions_.find(room_name);
|
||||
if (it == room_positions_.end()) { return; }
|
||||
const auto& pos = it->second.pos;
|
||||
|
||||
auto room_cx = static_cast<float>(cellPixelX(pos.x) + (CELL_W / 2));
|
||||
auto room_cy = static_cast<float>(cellPixelY(pos.y) + (CELL_H / 2));
|
||||
view_x_ = static_cast<float>(PlayArea::WIDTH) / 2.0F - room_cx;
|
||||
view_y_ = static_cast<float>(PlayArea::HEIGHT) / 2.0F - room_cy;
|
||||
}
|
||||
|
||||
// Devuelve el nombre de la room en una posición de pantalla, o vacío si no hay ninguna
|
||||
auto MiniMap::roomAtScreen(float screen_x, float screen_y) -> std::string {
|
||||
// Convertir coordenada de pantalla a coordenada dentro del minimapa
|
||||
float map_x = screen_x - view_x_;
|
||||
float map_y = screen_y - view_y_;
|
||||
|
||||
for (const auto& [name, mini] : room_positions_) {
|
||||
auto rx = static_cast<float>(cellPixelX(mini.pos.x));
|
||||
auto ry = static_cast<float>(cellPixelY(mini.pos.y));
|
||||
if (map_x >= rx && map_x < rx + CELL_W && map_y >= ry && map_y < ry + CELL_H) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Renderiza el minimapa
|
||||
void MiniMap::render(const std::string& current_room) {
|
||||
if (!map_surface_) { return; }
|
||||
|
||||
auto game_surface = Screen::get()->getRendererSurface();
|
||||
if (!game_surface) { return; }
|
||||
|
||||
// Renderizar la surface del minimapa con el viewport actual (alineado a pixel)
|
||||
float vx = std::floor(view_x_);
|
||||
float vy = std::floor(view_y_);
|
||||
SDL_FRect dst = {.x = vx, .y = vy, .w = static_cast<float>(map_width_ + SHADOW_OFFSET), .h = static_cast<float>(map_height_ + SHADOW_OFFSET)};
|
||||
map_surface_->render(nullptr, &dst);
|
||||
|
||||
// Highlight de la room actual (solo si está completamente visible en el play area)
|
||||
auto it = room_positions_.find(current_room);
|
||||
if (it != room_positions_.end()) {
|
||||
float cur_x = vx + static_cast<float>(cellPixelX(it->second.pos.x)) - 1;
|
||||
float cur_y = vy + static_cast<float>(cellPixelY(it->second.pos.y)) - 1;
|
||||
auto cur_w = static_cast<float>(CELL_W + 2);
|
||||
auto cur_h = static_cast<float>(CELL_H + 2);
|
||||
if (cur_x >= 0 && cur_y >= 0 && cur_x + cur_w <= PlayArea::WIDTH && cur_y + cur_h <= PlayArea::HEIGHT) {
|
||||
SDL_FRect highlight = {.x = cur_x, .y = cur_y, .w = cur_w, .h = cur_h};
|
||||
game_surface->drawRectBorder(&highlight, stringToColor("bright_white"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Maneja eventos del minimapa (drag para explorar, click para navegar)
|
||||
void MiniMap::handleEvent(const SDL_Event& event, const std::string& current_room) {
|
||||
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) {
|
||||
// Guardar posición inicial para detectar si es click o drag
|
||||
float mouse_x = 0.0F;
|
||||
float mouse_y = 0.0F;
|
||||
SDL_GetMouseState(&mouse_x, &mouse_y);
|
||||
float render_x = 0.0F;
|
||||
float render_y = 0.0F;
|
||||
SDL_RenderCoordinatesFromWindow(Screen::get()->getRenderer(), mouse_x, mouse_y, &render_x, &render_y);
|
||||
SDL_FRect dst_rect = Screen::get()->getGameSurfaceDstRect();
|
||||
|
||||
dragging_ = true;
|
||||
drag_start_x_ = render_x - dst_rect.x;
|
||||
drag_start_y_ = render_y - dst_rect.y;
|
||||
view_start_x_ = view_x_;
|
||||
view_start_y_ = view_y_;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_MOUSE_MOTION && dragging_) {
|
||||
float mouse_x = 0.0F;
|
||||
float mouse_y = 0.0F;
|
||||
SDL_GetMouseState(&mouse_x, &mouse_y);
|
||||
float render_x = 0.0F;
|
||||
float render_y = 0.0F;
|
||||
SDL_RenderCoordinatesFromWindow(Screen::get()->getRenderer(), mouse_x, mouse_y, &render_x, &render_y);
|
||||
SDL_FRect dst_rect = Screen::get()->getGameSurfaceDstRect();
|
||||
|
||||
float game_x = render_x - dst_rect.x;
|
||||
float game_y = render_y - dst_rect.y;
|
||||
view_x_ = view_start_x_ + (game_x - drag_start_x_);
|
||||
view_y_ = view_start_y_ + (game_y - drag_start_y_);
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT) {
|
||||
if (dragging_) {
|
||||
// Comprobar si fue click (sin mover) o drag
|
||||
float mouse_x = 0.0F;
|
||||
float mouse_y = 0.0F;
|
||||
SDL_GetMouseState(&mouse_x, &mouse_y);
|
||||
float render_x = 0.0F;
|
||||
float render_y = 0.0F;
|
||||
SDL_RenderCoordinatesFromWindow(Screen::get()->getRenderer(), mouse_x, mouse_y, &render_x, &render_y);
|
||||
SDL_FRect dst_rect = Screen::get()->getGameSurfaceDstRect();
|
||||
|
||||
float game_x = render_x - dst_rect.x;
|
||||
float game_y = render_y - dst_rect.y;
|
||||
|
||||
float dx = game_x - drag_start_x_;
|
||||
float dy = game_y - drag_start_y_;
|
||||
bool was_click = (dx * dx + dy * dy) < 4.0F; // Menos de 2px de movimiento = click
|
||||
|
||||
if (was_click) {
|
||||
// Click: navegar a la room bajo el cursor
|
||||
std::string room = roomAtScreen(game_x, game_y);
|
||||
if (!room.empty() && room != current_room && on_navigate) {
|
||||
on_navigate(room);
|
||||
}
|
||||
}
|
||||
|
||||
dragging_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // _DEBUG
|
||||
103
source/game/editor/mini_map.hpp
Normal file
103
source/game/editor/mini_map.hpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <functional> // Para function
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
#include <unordered_map> // Para unordered_map
|
||||
#include <vector> // Para vector
|
||||
|
||||
class Surface;
|
||||
|
||||
/**
|
||||
* @brief Minimapa global del juego para el editor
|
||||
*
|
||||
* Genera una vista en miniatura de todas las habitaciones del juego,
|
||||
* posicionadas según sus conexiones.
|
||||
* Cada tile del mapa se representa como 1 pixel del color predominante de ese tile.
|
||||
* Resultado: cada room = 32x16 pixels.
|
||||
*/
|
||||
class MiniMap {
|
||||
public:
|
||||
explicit MiniMap(Uint8 bg_color = 2, Uint8 conn_color = 14);
|
||||
~MiniMap() = default;
|
||||
|
||||
void render(const std::string& current_room);
|
||||
void handleEvent(const SDL_Event& event, const std::string& current_room);
|
||||
void rebuild(Uint8 bg_color, Uint8 conn_color);
|
||||
void centerOnRoom(const std::string& room_name);
|
||||
[[nodiscard]] auto isReady() const -> bool { return !room_positions_.empty(); }
|
||||
|
||||
// Callback al hacer click en una minihabitación (nombre del room)
|
||||
std::function<void(const std::string&)> on_navigate;
|
||||
|
||||
private:
|
||||
// Posición de una room en el grid del minimapa
|
||||
struct GridPos {
|
||||
int x{0};
|
||||
int y{0};
|
||||
};
|
||||
|
||||
// Una room renderizada
|
||||
struct RoomMini {
|
||||
std::shared_ptr<Surface> surface; // 32x16 pixels
|
||||
GridPos pos; // Posición en el grid
|
||||
};
|
||||
|
||||
void buildTileColorTable(const std::string& tileset_name);
|
||||
void buildRoomSurfaces();
|
||||
void layoutRooms();
|
||||
void composeFinalSurface();
|
||||
auto getRoomMiniSurface(const std::string& room_name) -> std::shared_ptr<Surface>;
|
||||
void drawConnections();
|
||||
auto roomAtScreen(float screen_x, float screen_y) -> std::string;
|
||||
auto cellPixelX(int grid_x) const -> int { return PADDING + ((grid_x - min_grid_x_) * (CELL_W + GAP)); }
|
||||
auto cellPixelY(int grid_y) const -> int { return PADDING + ((grid_y - min_grid_y_) * (CELL_H + GAP)); }
|
||||
|
||||
// Tabla de color predominante por tile index
|
||||
std::vector<Uint8> tile_colors_; // tile_index → palette color index
|
||||
int tileset_width_{0}; // Ancho del tileset en tiles
|
||||
Uint8 tileset_transparent_{16}; // Color transparente del tileset
|
||||
|
||||
// Rooms renderizadas y posicionadas
|
||||
std::unordered_map<std::string, RoomMini> room_positions_;
|
||||
|
||||
// Surface final compuesta
|
||||
std::shared_ptr<Surface> map_surface_;
|
||||
int map_width_{0}; // Ancho en pixels
|
||||
int map_height_{0}; // Alto en pixels
|
||||
|
||||
// Offset para normalizar coordenadas
|
||||
int min_grid_x_{0};
|
||||
int min_grid_y_{0};
|
||||
|
||||
// Viewport: offset de la surface del minimapa respecto al play area
|
||||
float view_x_{0.0F}; // Offset X actual
|
||||
float view_y_{0.0F}; // Offset Y actual
|
||||
bool dragging_{false};
|
||||
float drag_start_x_{0.0F}; // Posición del ratón al inicio del drag
|
||||
float drag_start_y_{0.0F};
|
||||
float view_start_x_{0.0F}; // Viewport al inicio del drag
|
||||
float view_start_y_{0.0F};
|
||||
|
||||
// Constantes
|
||||
static constexpr int ROOM_W = 32; // Ancho de una room en pixels del minimapa
|
||||
static constexpr int ROOM_H = 16; // Alto de una room en pixels del minimapa
|
||||
static constexpr int BORDER = 1; // Borde alrededor de cada room
|
||||
static constexpr int CELL_W = ROOM_W + (BORDER * 2); // Room + borde
|
||||
static constexpr int CELL_H = ROOM_H + (BORDER * 2);
|
||||
static constexpr int GAP = 4; // Separación entre celdas
|
||||
static constexpr int SHADOW_OFFSET = 1; // Desplazamiento de la sombra
|
||||
static constexpr int PADDING = 4; // Padding alrededor del minimapa
|
||||
|
||||
// Colores del minimapa (índices de paleta)
|
||||
Uint8 bg_color_{2}; // Fondo general (configurable)
|
||||
Uint8 conn_color_{14}; // Líneas de conexión (configurable)
|
||||
static constexpr Uint8 COLOR_ROOM_BORDER = 0; // Borde de cada miniroom
|
||||
static constexpr Uint8 COLOR_SHADOW = 1; // Sombra de cada miniroom
|
||||
};
|
||||
|
||||
#endif // _DEBUG
|
||||
178
source/game/editor/room_saver.cpp
Normal file
178
source/game/editor/room_saver.cpp
Normal file
@@ -0,0 +1,178 @@
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include "game/editor/room_saver.hpp"
|
||||
|
||||
#include <cmath> // Para std::round
|
||||
#include <fstream> // Para ifstream, ofstream, istreambuf_iterator
|
||||
#include <iostream> // Para cout, cerr
|
||||
#include <sstream> // Para ostringstream
|
||||
|
||||
#include "utils/defines.hpp" // Para Tile::SIZE
|
||||
|
||||
// Carga el YAML original directamente del filesystem (no del resource pack)
|
||||
auto RoomSaver::loadYAML(const std::string& file_path) -> fkyaml::node {
|
||||
std::ifstream file(file_path);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "RoomSaver: Cannot open " << file_path << "\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
return fkyaml::node::deserialize(content);
|
||||
}
|
||||
|
||||
// Convierte una room connection al formato YAML
|
||||
auto RoomSaver::roomConnectionToYAML(const std::string& connection) -> std::string {
|
||||
if (connection == "0" || connection.empty()) { return "null"; }
|
||||
return connection;
|
||||
}
|
||||
|
||||
// Convierte la dirección del conveyor belt a string
|
||||
auto RoomSaver::conveyorBeltToString(int direction) -> std::string {
|
||||
if (direction < 0) { return "left"; }
|
||||
if (direction > 0) { return "right"; }
|
||||
return "none";
|
||||
}
|
||||
|
||||
// Genera el YAML completo como texto con formato compacto
|
||||
auto RoomSaver::buildYAML(const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
std::ostringstream out;
|
||||
|
||||
// --- Cabecera: nombre como comentario ---
|
||||
out << "# " << room_data.name << "\n";
|
||||
|
||||
// --- Sección room ---
|
||||
out << "room:\n";
|
||||
|
||||
// Escribir todos los campos name_* del YAML original (preserva name_ca, name_en, etc.)
|
||||
if (original_yaml.contains("room")) {
|
||||
const auto& room_node = original_yaml["room"];
|
||||
for (auto it = room_node.begin(); it != room_node.end(); ++it) {
|
||||
const auto KEY = it.key().get_value<std::string>();
|
||||
if (KEY.substr(0, 5) == "name_") {
|
||||
out << " " << KEY << ": \"" << it.value().get_value<std::string>() << "\"\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out << " bgColor: " << room_data.bg_color << "\n";
|
||||
out << " border: " << room_data.border_color << "\n";
|
||||
out << " tileSetFile: " << room_data.tile_set_file << "\n";
|
||||
|
||||
// Conexiones
|
||||
out << "\n";
|
||||
out << " # Conexiones de la habitación (null = sin conexión)\n";
|
||||
out << " connections:\n";
|
||||
out << " up: " << roomConnectionToYAML(room_data.upper_room) << "\n";
|
||||
out << " down: " << roomConnectionToYAML(room_data.lower_room) << "\n";
|
||||
out << " left: " << roomConnectionToYAML(room_data.left_room) << "\n";
|
||||
out << " right: " << roomConnectionToYAML(room_data.right_room) << "\n";
|
||||
|
||||
// Colores de items
|
||||
out << "\n";
|
||||
out << " # Colores de los objetos\n";
|
||||
out << " itemColor1: " << (room_data.item_color1.empty() ? "yellow" : room_data.item_color1) << "\n";
|
||||
out << " itemColor2: " << (room_data.item_color2.empty() ? "magenta" : room_data.item_color2) << "\n";
|
||||
|
||||
// Conveyor belt
|
||||
out << "\n";
|
||||
out << " # Dirección de la cinta transportadora: left, none, right\n";
|
||||
out << " conveyorBelt: " << conveyorBeltToString(room_data.conveyor_belt_direction) << "\n";
|
||||
|
||||
// --- Tilemap (16 filas × 32 columnas, formato flow) ---
|
||||
out << "\n";
|
||||
out << "# Tilemap: 16 filas × 32 columnas (256×192 píxeles @ 8px/tile)\n";
|
||||
out << "# Índices de tiles (-1 = vacío)\n";
|
||||
out << "tilemap:\n";
|
||||
constexpr int MAP_WIDTH = 32;
|
||||
constexpr int MAP_HEIGHT = 16;
|
||||
for (int row = 0; row < MAP_HEIGHT; ++row) {
|
||||
out << " - [";
|
||||
for (int col = 0; col < MAP_WIDTH; ++col) {
|
||||
int index = (row * MAP_WIDTH) + col;
|
||||
if (index < static_cast<int>(room_data.tile_map.size())) {
|
||||
out << room_data.tile_map[index];
|
||||
} else {
|
||||
out << -1;
|
||||
}
|
||||
if (col < MAP_WIDTH - 1) { out << ", "; }
|
||||
}
|
||||
out << "]\n";
|
||||
}
|
||||
|
||||
// --- Enemigos ---
|
||||
if (!room_data.enemies.empty()) {
|
||||
out << "\n";
|
||||
out << "# Enemigos en esta habitación\n";
|
||||
out << "enemies:\n";
|
||||
for (const auto& enemy : room_data.enemies) {
|
||||
out << " - animation: " << enemy.animation_path << "\n";
|
||||
|
||||
int pos_x = static_cast<int>(std::round(enemy.x / Tile::SIZE));
|
||||
int pos_y = static_cast<int>(std::round(enemy.y / Tile::SIZE));
|
||||
out << " position: {x: " << pos_x << ", y: " << pos_y << "}\n";
|
||||
|
||||
out << " velocity: {x: " << enemy.vx << ", y: " << enemy.vy << "}\n";
|
||||
|
||||
int b1_x = enemy.x1 / Tile::SIZE;
|
||||
int b1_y = enemy.y1 / Tile::SIZE;
|
||||
int b2_x = enemy.x2 / Tile::SIZE;
|
||||
int b2_y = enemy.y2 / Tile::SIZE;
|
||||
out << " boundaries:\n";
|
||||
out << " position1: {x: " << b1_x << ", y: " << b1_y << "}\n";
|
||||
out << " position2: {x: " << b2_x << ", y: " << b2_y << "}\n";
|
||||
|
||||
if (!enemy.color.empty() && enemy.color != "white") {
|
||||
out << " color: " << enemy.color << "\n";
|
||||
}
|
||||
if (enemy.flip) { out << " flip: true\n"; }
|
||||
if (enemy.mirror) { out << " mirror: true\n"; }
|
||||
if (enemy.frame != -1) { out << " frame: " << enemy.frame << "\n"; }
|
||||
|
||||
out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// --- Items ---
|
||||
if (!room_data.items.empty()) {
|
||||
out << "# Objetos en esta habitación\n";
|
||||
out << "items:\n";
|
||||
for (const auto& item : room_data.items) {
|
||||
out << " - tileSetFile: " << item.tile_set_file << "\n";
|
||||
out << " tile: " << item.tile << "\n";
|
||||
|
||||
int item_x = static_cast<int>(std::round(item.x / Tile::SIZE));
|
||||
int item_y = static_cast<int>(std::round(item.y / Tile::SIZE));
|
||||
out << " position: {x: " << item_x << ", y: " << item_y << "}\n";
|
||||
|
||||
if (item.counter != 0) {
|
||||
out << " counter: " << item.counter << "\n";
|
||||
}
|
||||
|
||||
out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return out.str();
|
||||
}
|
||||
|
||||
// Guarda el YAML a disco
|
||||
auto RoomSaver::saveYAML(const std::string& file_path, const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string {
|
||||
std::string content = buildYAML(original_yaml, room_data);
|
||||
|
||||
std::ofstream file(file_path);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "RoomSaver: Cannot write to " << file_path << "\n";
|
||||
return "Error: Cannot write to " + file_path;
|
||||
}
|
||||
|
||||
file << content;
|
||||
file.close();
|
||||
|
||||
const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1);
|
||||
std::cout << "RoomSaver: Saved " << FILE_NAME << "\n";
|
||||
return "Saved " + FILE_NAME;
|
||||
}
|
||||
|
||||
#endif // _DEBUG
|
||||
35
source/game/editor/room_saver.hpp
Normal file
35
source/game/editor/room_saver.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include <string> // Para string
|
||||
|
||||
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||
#include "game/gameplay/room.hpp" // Para Room::Data
|
||||
|
||||
/**
|
||||
* @brief Guardado de archivos YAML de habitaciones para el editor de mapas
|
||||
*
|
||||
* Lee el YAML original con fkyaml (para acceder a todos los campos: name_ca, name_en, etc.)
|
||||
* Genera el YAML como texto formateado compacto (idéntico al formato original de los ficheros).
|
||||
* Solo se usa en builds de debug.
|
||||
*/
|
||||
class RoomSaver {
|
||||
public:
|
||||
RoomSaver() = delete;
|
||||
|
||||
// Carga el YAML original desde disco como nodo fkyaml (lee del filesystem, no del pack)
|
||||
static auto loadYAML(const std::string& file_path) -> fkyaml::node;
|
||||
|
||||
// Genera y guarda el YAML completo a disco
|
||||
// original_yaml: nodo fkyaml con los datos originales (para campos que no se editan: name_ca, etc.)
|
||||
// room_data: datos editados (posiciones de enemigos, items, etc.)
|
||||
static auto saveYAML(const std::string& file_path, const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string;
|
||||
|
||||
private:
|
||||
static auto buildYAML(const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string;
|
||||
static auto roomConnectionToYAML(const std::string& connection) -> std::string;
|
||||
static auto conveyorBeltToString(int direction) -> std::string;
|
||||
};
|
||||
|
||||
#endif // _DEBUG
|
||||
240
source/game/editor/tile_picker.cpp
Normal file
240
source/game/editor/tile_picker.cpp
Normal file
@@ -0,0 +1,240 @@
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include "game/editor/tile_picker.hpp"
|
||||
|
||||
#include <algorithm> // Para std::clamp, std::min
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource::Cache
|
||||
#include "utils/defines.hpp" // Para Tile::SIZE, PlayArea
|
||||
#include "utils/utils.hpp" // Para stringToColor
|
||||
|
||||
// Margen del borde alrededor del tileset (en pixels)
|
||||
static constexpr int BORDER_PAD = 3;
|
||||
|
||||
// Abre el picker con un tileset
|
||||
void TilePicker::open(const std::string& tileset_name, int current_tile, int bg_color, int source_color, int target_color, int tile_spacing_in, int tile_spacing_out) {
|
||||
tileset_ = Resource::Cache::get()->getSurface(tileset_name);
|
||||
if (!tileset_) {
|
||||
open_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
spacing_in_ = tile_spacing_in;
|
||||
spacing_out_ = tile_spacing_out;
|
||||
|
||||
// Calcular dimensiones del tileset en tiles (teniendo en cuenta spacing de entrada)
|
||||
int src_cell = Tile::SIZE + spacing_in_;
|
||||
tileset_width_ = static_cast<int>(tileset_->getWidth()) / src_cell;
|
||||
tileset_height_ = static_cast<int>(tileset_->getHeight()) / src_cell;
|
||||
// Corregir si el último tile cabe sin spacing
|
||||
if (tileset_width_ == 0 && tileset_->getWidth() >= Tile::SIZE) { tileset_width_ = 1; }
|
||||
if (tileset_height_ == 0 && tileset_->getHeight() >= Tile::SIZE) { tileset_height_ = 1; }
|
||||
|
||||
current_tile_ = current_tile;
|
||||
hover_tile_ = -1;
|
||||
scroll_y_ = 0;
|
||||
|
||||
// Dimensiones de salida (con spacing visual entre tiles)
|
||||
int out_cell = Tile::SIZE + spacing_out_;
|
||||
int display_w = (tileset_width_ * out_cell) - spacing_out_; // Sin trailing
|
||||
int display_h = (tileset_height_ * out_cell) - spacing_out_;
|
||||
|
||||
// Frame: display + borde
|
||||
int frame_w = display_w + (BORDER_PAD * 2);
|
||||
int frame_h = display_h + (BORDER_PAD * 2);
|
||||
frame_surface_ = std::make_shared<Surface>(frame_w, frame_h);
|
||||
|
||||
// Componer: fondo + borde + tiles uno a uno
|
||||
{
|
||||
auto prev = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(frame_surface_);
|
||||
|
||||
Uint8 fill_color = (bg_color >= 0) ? static_cast<Uint8>(bg_color) : stringToColor("black");
|
||||
frame_surface_->clear(fill_color);
|
||||
|
||||
// Borde doble
|
||||
SDL_FRect outer = {.x = 0, .y = 0, .w = static_cast<float>(frame_w), .h = static_cast<float>(frame_h)};
|
||||
frame_surface_->drawRectBorder(&outer, stringToColor("bright_white"));
|
||||
SDL_FRect inner = {.x = 1, .y = 1, .w = static_cast<float>(frame_w - 2), .h = static_cast<float>(frame_h - 2)};
|
||||
frame_surface_->drawRectBorder(&inner, stringToColor("white"));
|
||||
|
||||
// Renderizar cada tile individualmente
|
||||
constexpr auto TS = static_cast<float>(Tile::SIZE);
|
||||
for (int row = 0; row < tileset_height_; ++row) {
|
||||
for (int col = 0; col < tileset_width_; ++col) {
|
||||
// Fuente: posición en el tileset original
|
||||
SDL_FRect src = {
|
||||
.x = static_cast<float>(col * src_cell),
|
||||
.y = static_cast<float>(row * src_cell),
|
||||
.w = TS,
|
||||
.h = TS};
|
||||
|
||||
// Destino: posición en el frame con spacing de salida
|
||||
int dst_x = BORDER_PAD + (col * out_cell);
|
||||
int dst_y = BORDER_PAD + (row * out_cell);
|
||||
|
||||
if (source_color >= 0 && target_color >= 0) {
|
||||
tileset_->renderWithColorReplace(dst_x, dst_y, static_cast<Uint8>(source_color), static_cast<Uint8>(target_color), &src);
|
||||
} else {
|
||||
SDL_FRect dst = {.x = static_cast<float>(dst_x), .y = static_cast<float>(dst_y), .w = TS, .h = TS};
|
||||
tileset_->render(&src, &dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Screen::get()->setRendererSurface(prev);
|
||||
}
|
||||
|
||||
// Centrar en el play area
|
||||
offset_x_ = (PlayArea::WIDTH - frame_w) / 2;
|
||||
int offset_y = (PlayArea::HEIGHT - frame_h) / 2;
|
||||
offset_y = std::max(offset_y, 0);
|
||||
|
||||
visible_height_ = PlayArea::HEIGHT;
|
||||
|
||||
frame_dst_ = {.x = static_cast<float>(offset_x_), .y = static_cast<float>(offset_y), .w = static_cast<float>(frame_w), .h = static_cast<float>(frame_h)};
|
||||
|
||||
// Si el frame es más alto que el play area, necesitará scroll
|
||||
if (frame_h > visible_height_) {
|
||||
frame_dst_.y = 0;
|
||||
if (current_tile_ >= 0) {
|
||||
int tile_row = current_tile_ / tileset_width_;
|
||||
int tile_y_px = tile_row * out_cell;
|
||||
if (tile_y_px > visible_height_ / 2) {
|
||||
scroll_y_ = tile_y_px - visible_height_ / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open_ = true;
|
||||
updateMousePosition();
|
||||
}
|
||||
|
||||
// Cierra el picker
|
||||
void TilePicker::close() {
|
||||
open_ = false;
|
||||
tileset_.reset();
|
||||
frame_surface_.reset();
|
||||
}
|
||||
|
||||
// Renderiza el picker
|
||||
void TilePicker::render() {
|
||||
if (!open_ || !frame_surface_) { return; }
|
||||
|
||||
auto game_surface = Screen::get()->getRendererSurface();
|
||||
if (!game_surface) { return; }
|
||||
|
||||
int frame_h = static_cast<int>(frame_dst_.h);
|
||||
|
||||
if (frame_h <= visible_height_) {
|
||||
frame_surface_->render(nullptr, &frame_dst_);
|
||||
} else {
|
||||
int max_scroll = frame_h - visible_height_;
|
||||
scroll_y_ = std::clamp(scroll_y_, 0, max_scroll);
|
||||
|
||||
SDL_FRect src = {.x = 0, .y = static_cast<float>(scroll_y_), .w = frame_dst_.w, .h = static_cast<float>(visible_height_)};
|
||||
SDL_FRect dst = {.x = frame_dst_.x, .y = 0, .w = frame_dst_.w, .h = static_cast<float>(visible_height_)};
|
||||
frame_surface_->render(&src, &dst);
|
||||
}
|
||||
|
||||
// Highlights (en game_surface, encima del frame)
|
||||
int out_cell = Tile::SIZE + spacing_out_;
|
||||
float tileset_screen_x = frame_dst_.x + BORDER_PAD;
|
||||
float tileset_screen_y = frame_dst_.y + BORDER_PAD - static_cast<float>(scroll_y_);
|
||||
constexpr auto TS = static_cast<float>(Tile::SIZE);
|
||||
|
||||
// Highlight del tile bajo el cursor (blanco)
|
||||
if (hover_tile_ >= 0) {
|
||||
int col = hover_tile_ % tileset_width_;
|
||||
int row = hover_tile_ / tileset_width_;
|
||||
float hx = tileset_screen_x + static_cast<float>(col * out_cell);
|
||||
float hy = tileset_screen_y + static_cast<float>(row * out_cell);
|
||||
if (hy >= 0 && hy + TS <= visible_height_) {
|
||||
SDL_FRect highlight = {.x = hx, .y = hy, .w = TS, .h = TS};
|
||||
game_surface->drawRectBorder(&highlight, stringToColor("bright_white"));
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight del tile actual (verde)
|
||||
if (current_tile_ >= 0) {
|
||||
int col = current_tile_ % tileset_width_;
|
||||
int row = current_tile_ / tileset_width_;
|
||||
float cx = tileset_screen_x + static_cast<float>(col * out_cell);
|
||||
float cy = tileset_screen_y + static_cast<float>(row * out_cell);
|
||||
if (cy >= 0 && cy + TS <= visible_height_) {
|
||||
SDL_FRect cur_rect = {.x = cx, .y = cy, .w = TS, .h = TS};
|
||||
game_surface->drawRectBorder(&cur_rect, stringToColor("bright_green"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Maneja eventos del picker
|
||||
void TilePicker::handleEvent(const SDL_Event& event) {
|
||||
if (!open_) { return; }
|
||||
|
||||
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||
updateMousePosition();
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
|
||||
if (event.button.button == SDL_BUTTON_LEFT && hover_tile_ >= 0) {
|
||||
if (on_select) { on_select(hover_tile_); }
|
||||
close();
|
||||
} else if (event.button.button == SDL_BUTTON_RIGHT) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_MOUSE_WHEEL) {
|
||||
scroll_y_ -= static_cast<int>(event.wheel.y) * Tile::SIZE * 2;
|
||||
int max_scroll = static_cast<int>(frame_dst_.h) - visible_height_;
|
||||
max_scroll = std::max(max_scroll, 0);
|
||||
scroll_y_ = std::clamp(scroll_y_, 0, max_scroll);
|
||||
updateMousePosition();
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
// Calcula qué tile está bajo el cursor
|
||||
void TilePicker::updateMousePosition() {
|
||||
float mouse_x = 0.0F;
|
||||
float mouse_y = 0.0F;
|
||||
SDL_GetMouseState(&mouse_x, &mouse_y);
|
||||
|
||||
float render_x = 0.0F;
|
||||
float render_y = 0.0F;
|
||||
SDL_RenderCoordinatesFromWindow(Screen::get()->getRenderer(), mouse_x, mouse_y, &render_x, &render_y);
|
||||
|
||||
SDL_FRect dst_rect = Screen::get()->getGameSurfaceDstRect();
|
||||
float game_x = render_x - dst_rect.x;
|
||||
float game_y = render_y - dst_rect.y;
|
||||
|
||||
// Coordenada relativa al contenido del frame (con scroll)
|
||||
float rel_x = game_x - frame_dst_.x - BORDER_PAD;
|
||||
float rel_y = game_y - frame_dst_.y - BORDER_PAD + static_cast<float>(scroll_y_);
|
||||
|
||||
// Convertir a tile teniendo en cuenta el spacing de salida
|
||||
int out_cell = Tile::SIZE + spacing_out_;
|
||||
int tile_x = static_cast<int>(rel_x) / out_cell;
|
||||
int tile_y = static_cast<int>(rel_y) / out_cell;
|
||||
|
||||
// Verificar que estamos sobre un tile y no sobre el spacing
|
||||
int local_x = static_cast<int>(rel_x) % out_cell;
|
||||
int local_y = static_cast<int>(rel_y) % out_cell;
|
||||
bool on_tile = (local_x < Tile::SIZE && local_y < Tile::SIZE);
|
||||
|
||||
if (on_tile && rel_x >= 0 && rel_y >= 0 &&
|
||||
tile_x >= 0 && tile_x < tileset_width_ &&
|
||||
tile_y >= 0 && tile_y < tileset_height_) {
|
||||
hover_tile_ = tile_y * tileset_width_ + tile_x;
|
||||
} else {
|
||||
hover_tile_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // _DEBUG
|
||||
64
source/game/editor/tile_picker.hpp
Normal file
64
source/game/editor/tile_picker.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <functional> // Para function
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
|
||||
class Surface;
|
||||
|
||||
/**
|
||||
* @brief Selector visual de tiles de un tileset
|
||||
*
|
||||
* Muestra el tileset centrado en el play area.
|
||||
* Hover ilumina el tile bajo el cursor.
|
||||
* Click selecciona el tile y cierra el picker.
|
||||
* Mouse wheel para scroll si el tileset es más alto que el play area.
|
||||
* ESC o click derecho para cancelar.
|
||||
*/
|
||||
class TilePicker {
|
||||
public:
|
||||
TilePicker() = default;
|
||||
~TilePicker() = default;
|
||||
|
||||
// Abre el picker con un tileset
|
||||
// bg_color: color de fondo del panel (-1 = negro)
|
||||
// source_color/target_color: sustitución de color (-1 = sin sustitución)
|
||||
// tile_spacing_in: pixels de separación entre tiles en el fichero fuente
|
||||
// tile_spacing_out: pixels de separación visual entre tiles al mostrar
|
||||
void open(const std::string& tileset_name, int current_tile = -1, int bg_color = -1, int source_color = -1, int target_color = -1, int tile_spacing_in = 0, int tile_spacing_out = 1);
|
||||
void close();
|
||||
[[nodiscard]] auto isOpen() const -> bool { return open_; }
|
||||
|
||||
void render();
|
||||
void handleEvent(const SDL_Event& event);
|
||||
|
||||
// Callback al seleccionar un tile (índice del tile)
|
||||
std::function<void(int)> on_select;
|
||||
|
||||
private:
|
||||
void updateMousePosition();
|
||||
|
||||
bool open_{false};
|
||||
std::shared_ptr<Surface> tileset_; // Surface del tileset original
|
||||
std::shared_ptr<Surface> frame_surface_; // Surface compuesta: borde + tileset
|
||||
SDL_FRect frame_dst_{}; // Posición del frame en pantalla
|
||||
int tileset_width_{0}; // Ancho del tileset en tiles
|
||||
int tileset_height_{0}; // Alto del tileset en tiles
|
||||
int current_tile_{-1}; // Tile actualmente seleccionado (highlight)
|
||||
int hover_tile_{-1}; // Tile bajo el cursor
|
||||
|
||||
// Spacing
|
||||
int spacing_in_{0}; // Spacing en el fichero fuente
|
||||
int spacing_out_{1}; // Spacing visual al mostrar
|
||||
|
||||
// Scroll y posicionamiento
|
||||
int scroll_y_{0}; // Scroll vertical en pixels
|
||||
int offset_x_{0}; // Offset X para centrar en pantalla
|
||||
int visible_height_{0}; // Altura visible en pixels
|
||||
};
|
||||
|
||||
#endif // _DEBUG
|
||||
117
source/game/entities/enemy.cpp
Normal file
117
source/game/entities/enemy.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
#include "game/entities/enemy.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdlib> // Para rand
|
||||
|
||||
#include "core/rendering/sprite/animated_sprite.hpp" // Para SAnimatedSprite
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "utils/utils.hpp" // Para stringToColor
|
||||
|
||||
// Constructor
|
||||
Enemy::Enemy(const Data& enemy)
|
||||
: sprite_(std::make_shared<AnimatedSprite>(Resource::Cache::get()->getAnimationData(enemy.animation_path))),
|
||||
color_string_(enemy.color),
|
||||
x1_(enemy.x1),
|
||||
x2_(enemy.x2),
|
||||
y1_(enemy.y1),
|
||||
y2_(enemy.y2),
|
||||
should_flip_(enemy.flip),
|
||||
should_mirror_(enemy.mirror) {
|
||||
// Obten el resto de valores
|
||||
sprite_->setPosX(enemy.x);
|
||||
sprite_->setPosY(enemy.y);
|
||||
sprite_->setVelX(enemy.vx);
|
||||
sprite_->setVelY(enemy.vy);
|
||||
|
||||
const int FLIP = (should_flip_ && enemy.vx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
||||
const int MIRROR = should_mirror_ ? SDL_FLIP_VERTICAL : SDL_FLIP_NONE;
|
||||
sprite_->setFlip(static_cast<SDL_FlipMode>(FLIP | MIRROR)); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) SDL flags are designed for bitwise OR
|
||||
|
||||
collider_ = getRect();
|
||||
|
||||
color_ = stringToColor(color_string_);
|
||||
|
||||
// Coloca un frame al azar o el designado
|
||||
sprite_->setCurrentAnimationFrame((enemy.frame == -1) ? (rand() % sprite_->getCurrentAnimationSize()) : enemy.frame);
|
||||
}
|
||||
|
||||
// Pinta el enemigo en pantalla
|
||||
void Enemy::render() {
|
||||
sprite_->render(1, color_);
|
||||
}
|
||||
|
||||
// Actualiza las variables del objeto
|
||||
void Enemy::update(float delta_time) {
|
||||
sprite_->update(delta_time);
|
||||
checkPath();
|
||||
collider_ = getRect();
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Solo actualiza la animación sin mover al enemigo
|
||||
void Enemy::updateAnimation(float delta_time) {
|
||||
sprite_->animate(delta_time);
|
||||
}
|
||||
|
||||
// Resetea el enemigo a su posición inicial (para editor)
|
||||
void Enemy::resetToInitialPosition(const Data& data) {
|
||||
sprite_->setPosX(data.x);
|
||||
sprite_->setPosY(data.y);
|
||||
sprite_->setVelX(data.vx);
|
||||
sprite_->setVelY(data.vy);
|
||||
|
||||
const int FLIP = (should_flip_ && data.vx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
||||
const int MIRROR = should_mirror_ ? SDL_FLIP_VERTICAL : SDL_FLIP_NONE;
|
||||
sprite_->setFlip(static_cast<SDL_FlipMode>(FLIP | MIRROR)); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange)
|
||||
|
||||
collider_ = getRect();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Comprueba si ha llegado al limite del recorrido para darse media vuelta
|
||||
void Enemy::checkPath() { // NOLINT(readability-make-member-function-const)
|
||||
if (sprite_->getPosX() > x2_ || sprite_->getPosX() < x1_) {
|
||||
// Recoloca
|
||||
if (sprite_->getPosX() > x2_) {
|
||||
sprite_->setPosX(x2_);
|
||||
} else {
|
||||
sprite_->setPosX(x1_);
|
||||
}
|
||||
|
||||
// Cambia el sentido
|
||||
sprite_->setVelX(sprite_->getVelX() * (-1));
|
||||
|
||||
// Invierte el sprite
|
||||
if (should_flip_) {
|
||||
sprite_->flip();
|
||||
}
|
||||
}
|
||||
|
||||
if (sprite_->getPosY() > y2_ || sprite_->getPosY() < y1_) {
|
||||
// Recoloca
|
||||
if (sprite_->getPosY() > y2_) {
|
||||
sprite_->setPosY(y2_);
|
||||
} else {
|
||||
sprite_->setPosY(y1_);
|
||||
}
|
||||
|
||||
// Cambia el sentido
|
||||
sprite_->setVelY(sprite_->getVelY() * (-1));
|
||||
|
||||
// Invierte el sprite
|
||||
if (should_flip_) {
|
||||
sprite_->flip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Devuelve el rectangulo que contiene al enemigo
|
||||
auto Enemy::getRect() -> SDL_FRect {
|
||||
return sprite_->getRect();
|
||||
}
|
||||
|
||||
// Obtiene el rectangulo de colision del enemigo
|
||||
auto Enemy::getCollider() -> SDL_FRect& {
|
||||
return collider_;
|
||||
}
|
||||
55
source/game/entities/enemy.hpp
Normal file
55
source/game/entities/enemy.hpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
class AnimatedSprite; // lines 7-7
|
||||
|
||||
class Enemy {
|
||||
public:
|
||||
struct Data {
|
||||
std::string animation_path; // Ruta al fichero con la animación
|
||||
float x{0.0F}; // Posición inicial en el eje X
|
||||
float y{0.0F}; // Posición inicial en el eje Y
|
||||
float vx{0.0F}; // Velocidad en el eje X
|
||||
float vy{0.0F}; // Velocidad en el eje Y
|
||||
int x1{0}; // Límite izquierdo de la ruta en el eje X
|
||||
int x2{0}; // Límite derecho de la ruta en el eje X
|
||||
int y1{0}; // Límite superior de la ruta en el eje Y
|
||||
int y2{0}; // Límite inferior de la ruta en el eje Y
|
||||
bool flip{false}; // Indica si el enemigo hace flip al terminar su ruta
|
||||
bool mirror{false}; // Indica si el enemigo está volteado verticalmente
|
||||
int frame{0}; // Frame inicial para la animación del enemigo
|
||||
std::string color; // Color del enemigo
|
||||
};
|
||||
|
||||
explicit Enemy(const Data& enemy); // Constructor
|
||||
~Enemy() = default; // Destructor
|
||||
|
||||
void render(); // Pinta el enemigo en pantalla
|
||||
void update(float delta_time); // Actualiza las variables del objeto
|
||||
#ifdef _DEBUG
|
||||
void updateAnimation(float delta_time); // Solo actualiza la animación sin mover al enemigo
|
||||
void resetToInitialPosition(const Data& data); // Resetea el enemigo a su posición inicial (para editor)
|
||||
#endif
|
||||
|
||||
auto getRect() -> SDL_FRect; // Devuelve el rectangulo que contiene al enemigo
|
||||
auto getCollider() -> SDL_FRect&; // Obtiene el rectangulo de colision del enemigo
|
||||
|
||||
private:
|
||||
void checkPath(); // Comprueba si ha llegado al limite del recorrido para darse media vuelta
|
||||
|
||||
std::shared_ptr<AnimatedSprite> sprite_; // Sprite del enemigo
|
||||
|
||||
// Variables
|
||||
Uint8 color_{0}; // Color del enemigo
|
||||
std::string color_string_; // Color del enemigo en formato texto
|
||||
int x1_{0}; // Limite izquierdo de la ruta en el eje X
|
||||
int x2_{0}; // Limite derecho de la ruta en el eje X
|
||||
int y1_{0}; // Limite superior de la ruta en el eje Y
|
||||
int y2_{0}; // Limite inferior de la ruta en el eje Y
|
||||
SDL_FRect collider_{}; // Caja de colisión
|
||||
bool should_flip_{false}; // Indica si el enemigo hace flip al terminar su ruta
|
||||
bool should_mirror_{false}; // Indica si el enemigo se dibuja volteado verticalmente
|
||||
};
|
||||
71
source/game/entities/item.cpp
Normal file
71
source/game/entities/item.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
#include "game/entities/item.hpp"
|
||||
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
|
||||
// Constructor
|
||||
Item::Item(const Data& item)
|
||||
: sprite_(std::make_shared<Sprite>(Resource::Cache::get()->getSurface(item.tile_set_file), item.x, item.y, ITEM_SIZE, ITEM_SIZE)),
|
||||
time_accumulator_(static_cast<float>(item.counter) * COLOR_CHANGE_INTERVAL) {
|
||||
// Inicia variables
|
||||
sprite_->setClip((item.tile % 10) * ITEM_SIZE, (item.tile / 10) * ITEM_SIZE, ITEM_SIZE, ITEM_SIZE);
|
||||
collider_ = sprite_->getRect();
|
||||
|
||||
// Inicializa los colores
|
||||
color_.push_back(item.color1);
|
||||
color_.push_back(item.color1);
|
||||
|
||||
color_.push_back(item.color2);
|
||||
color_.push_back(item.color2);
|
||||
}
|
||||
|
||||
// Actualiza las variables del objeto
|
||||
void Item::update(float delta_time) {
|
||||
if (is_paused_) {
|
||||
return;
|
||||
}
|
||||
|
||||
time_accumulator_ += delta_time;
|
||||
}
|
||||
|
||||
// Pinta el objeto en pantalla
|
||||
void Item::render() const { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Calcula el índice de color basado en el tiempo acumulado
|
||||
const int INDEX = static_cast<int>(time_accumulator_ / COLOR_CHANGE_INTERVAL) % static_cast<int>(color_.size());
|
||||
sprite_->render(1, color_.at(INDEX));
|
||||
}
|
||||
|
||||
// Obtiene su ubicación
|
||||
auto Item::getPos() -> SDL_FPoint { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const SDL_FPoint P = {.x = sprite_->getX(), .y = sprite_->getY()};
|
||||
return P;
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Establece la posición del item (para editor)
|
||||
void Item::setPosition(float x, float y) {
|
||||
sprite_->setPosition(x, y);
|
||||
collider_ = sprite_->getRect();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Cambia el tile del item (para editor)
|
||||
void Item::setTile(int tile) {
|
||||
sprite_->setClip((tile % 10) * ITEM_SIZE, (tile / 10) * ITEM_SIZE, ITEM_SIZE, ITEM_SIZE);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Asigna los colores del objeto
|
||||
void Item::setColors(Uint8 col1, Uint8 col2) {
|
||||
// Reinicializa el vector de colores
|
||||
color_.clear();
|
||||
|
||||
// Añade el primer color
|
||||
color_.push_back(col1);
|
||||
color_.push_back(col1);
|
||||
|
||||
// Añade el segundo color
|
||||
color_.push_back(col2);
|
||||
color_.push_back(col2);
|
||||
}
|
||||
48
source/game/entities/item.hpp
Normal file
48
source/game/entities/item.hpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
#include <vector> // Para vector
|
||||
class Sprite;
|
||||
|
||||
class Item {
|
||||
public:
|
||||
struct Data {
|
||||
std::string tile_set_file; // Ruta al fichero con los gráficos del item
|
||||
float x{0.0F}; // Posición del item en pantalla
|
||||
float y{0.0F}; // Posición del item en pantalla
|
||||
int tile{0}; // Número de tile dentro de la textura
|
||||
int counter{0}; // Contador inicial. Es el que lo hace cambiar de color
|
||||
Uint8 color1{0}; // Uno de los dos colores que se utiliza para el item
|
||||
Uint8 color2{0}; // Uno de los dos colores que se utiliza para el item
|
||||
};
|
||||
|
||||
explicit Item(const Data& item); // Constructor
|
||||
~Item() = default; // Destructor
|
||||
|
||||
void render() const; // Pinta el objeto en pantalla
|
||||
void update(float delta_time); // Actualiza las variables del objeto
|
||||
|
||||
void setPaused(bool paused) { is_paused_ = paused; } // Pausa/despausa el item
|
||||
auto getCollider() -> SDL_FRect& { return collider_; } // Obtiene el rectangulo de colision del objeto
|
||||
auto getPos() -> SDL_FPoint; // Obtiene su ubicación
|
||||
void setColors(Uint8 col1, Uint8 col2); // Asigna los colores del objeto
|
||||
#ifdef _DEBUG
|
||||
void setPosition(float x, float y); // Establece la posición del item (para editor)
|
||||
void setTile(int tile); // Cambia el tile del item (para editor)
|
||||
#endif
|
||||
|
||||
private:
|
||||
static constexpr float ITEM_SIZE = 8.0F; // Tamaño del item en pixels
|
||||
static constexpr float COLOR_CHANGE_INTERVAL = 0.06F; // Intervalo de cambio de color en segundos (4 frames a 66.67fps)
|
||||
|
||||
std::shared_ptr<Sprite> sprite_; // SSprite del objeto
|
||||
|
||||
// Variables
|
||||
std::vector<Uint8> color_; // Vector con los colores del objeto
|
||||
float time_accumulator_{0.0F}; // Acumulador de tiempo para cambio de color
|
||||
SDL_FRect collider_{}; // Rectangulo de colisión
|
||||
bool is_paused_{false}; // Indica si el item está pausado
|
||||
};
|
||||
972
source/game/entities/player.cpp
Normal file
972
source/game/entities/player.cpp
Normal file
@@ -0,0 +1,972 @@
|
||||
// IWYU pragma: no_include <bits/std_abs.h>
|
||||
#include "game/entities/player.hpp"
|
||||
|
||||
#include <algorithm> // Para max, min
|
||||
#include <cmath> // Para ceil, abs
|
||||
#include <iostream>
|
||||
#include <ranges> // Para std::ranges::any_of
|
||||
|
||||
#include "core/audio/audio.hpp" // Para Audio
|
||||
#include "core/input/input.hpp" // Para Input, InputAction
|
||||
#include "core/rendering/sprite/animated_sprite.hpp" // Para SAnimatedSprite
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "game/gameplay/room.hpp" // Para Room, TileType
|
||||
#include "game/options.hpp" // Para Cheat, Options, options
|
||||
#include "utils/defines.hpp" // Para RoomBorder::BOTTOM, RoomBorder::LEFT, RoomBorder::RIGHT
|
||||
|
||||
#ifdef _DEBUG
|
||||
#include "core/system/debug.hpp" // Para Debug
|
||||
#endif
|
||||
|
||||
// Constructor
|
||||
Player::Player(const Data& player)
|
||||
: room_(player.room) {
|
||||
initSprite(player.animations_path);
|
||||
setColor();
|
||||
applySpawnValues(player.spawn_data);
|
||||
placeSprite();
|
||||
initSounds();
|
||||
|
||||
previous_state_ = state_;
|
||||
}
|
||||
|
||||
// Pinta el jugador en pantalla
|
||||
void Player::render() {
|
||||
sprite_->render(1, color_);
|
||||
#ifdef _DEBUG
|
||||
if (Debug::get()->isEnabled()) {
|
||||
Screen::get()->getRendererSurface()->putPixel(under_right_foot_.x, under_right_foot_.y, static_cast<Uint8>(PaletteColor::GREEN));
|
||||
Screen::get()->getRendererSurface()->putPixel(under_left_foot_.x, under_left_foot_.y, static_cast<Uint8>(PaletteColor::GREEN));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Actualiza las variables del objeto
|
||||
void Player::update(float delta_time) {
|
||||
if (!is_paused_) {
|
||||
handleInput();
|
||||
updateState(delta_time);
|
||||
move(delta_time);
|
||||
handleKillingTiles(); // Los collider_points_ están actualizados por syncSpriteAndCollider() dentro de move()
|
||||
animate(delta_time);
|
||||
border_ = handleBorders();
|
||||
}
|
||||
}
|
||||
|
||||
// Comprueba las entradas y modifica variables
|
||||
void Player::handleInput() {
|
||||
if (ignore_input_) { return; }
|
||||
if (Input::get()->checkAction(InputAction::LEFT)) {
|
||||
wanna_go_ = Direction::LEFT;
|
||||
} else if (Input::get()->checkAction(InputAction::RIGHT)) {
|
||||
wanna_go_ = Direction::RIGHT;
|
||||
} else {
|
||||
wanna_go_ = Direction::NONE;
|
||||
}
|
||||
|
||||
wanna_jump_ = Input::get()->checkAction(InputAction::JUMP);
|
||||
}
|
||||
|
||||
// La lógica de movimiento está distribuida en move
|
||||
void Player::move(float delta_time) {
|
||||
switch (state_) {
|
||||
case State::ON_GROUND:
|
||||
moveOnGround(delta_time);
|
||||
break;
|
||||
case State::ON_SLOPE:
|
||||
moveOnSlope(delta_time);
|
||||
break;
|
||||
case State::JUMPING:
|
||||
moveJumping(delta_time);
|
||||
break;
|
||||
case State::FALLING:
|
||||
moveFalling(delta_time);
|
||||
break;
|
||||
}
|
||||
syncSpriteAndCollider(); // Actualiza la posición del sprite y las colisiones
|
||||
#ifdef _DEBUG
|
||||
Debug::get()->set("P.X", std::to_string(static_cast<int>(x_)));
|
||||
Debug::get()->set("P.Y", std::to_string(static_cast<int>(y_)));
|
||||
Debug::get()->set("P.LGP", std::to_string(last_grounded_position_));
|
||||
switch (state_) {
|
||||
case State::ON_GROUND:
|
||||
Debug::get()->set("P.STATE", "ON_GROUND");
|
||||
break;
|
||||
case State::ON_SLOPE:
|
||||
Debug::get()->set("P.STATE", "ON_SLOPE");
|
||||
break;
|
||||
case State::JUMPING:
|
||||
Debug::get()->set("P.STATE", "JUMPING");
|
||||
break;
|
||||
case State::FALLING:
|
||||
Debug::get()->set("P.STATE", "FALLING");
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Player::handleConveyorBelts() {
|
||||
if (!auto_movement_ and isOnConveyorBelt() and wanna_go_ == Direction::NONE) {
|
||||
auto_movement_ = true;
|
||||
}
|
||||
|
||||
if (auto_movement_ and !isOnConveyorBelt()) {
|
||||
auto_movement_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Player::handleShouldFall() {
|
||||
if (!isOnFloor() and (state_ == State::ON_GROUND || state_ == State::ON_SLOPE)) {
|
||||
transitionToState(State::FALLING);
|
||||
}
|
||||
}
|
||||
|
||||
void Player::transitionToState(State state) {
|
||||
previous_state_ = state_;
|
||||
state_ = state;
|
||||
|
||||
switch (state) {
|
||||
case State::ON_GROUND:
|
||||
vy_ = 0;
|
||||
handleDeathByFalling();
|
||||
resetSoundControllersOnLanding();
|
||||
current_slope_ = nullptr;
|
||||
break;
|
||||
case State::ON_SLOPE:
|
||||
vy_ = 0;
|
||||
handleDeathByFalling();
|
||||
resetSoundControllersOnLanding();
|
||||
updateCurrentSlope();
|
||||
if (current_slope_ == nullptr) {
|
||||
// Los pies no coinciden con ninguna rampa: tratar como suelo plano
|
||||
state_ = State::ON_GROUND;
|
||||
}
|
||||
break;
|
||||
case State::JUMPING:
|
||||
// Puede saltar desde ON_GROUND o ON_SLOPE
|
||||
if (previous_state_ == State::ON_GROUND || previous_state_ == State::ON_SLOPE) {
|
||||
vy_ = -MAX_VY;
|
||||
last_grounded_position_ = y_;
|
||||
updateVelocity();
|
||||
jump_sound_ctrl_.start();
|
||||
current_slope_ = nullptr;
|
||||
}
|
||||
break;
|
||||
case State::FALLING:
|
||||
fall_start_position_ = static_cast<int>(y_);
|
||||
last_grounded_position_ = static_cast<int>(y_);
|
||||
vy_ = MAX_VY;
|
||||
vx_ = 0.0F;
|
||||
jump_sound_ctrl_.reset();
|
||||
fall_sound_ctrl_.start(y_);
|
||||
current_slope_ = nullptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Player::updateState(float delta_time) {
|
||||
switch (state_) {
|
||||
case State::ON_GROUND:
|
||||
updateOnGround(delta_time);
|
||||
break;
|
||||
case State::ON_SLOPE:
|
||||
updateOnSlope(delta_time);
|
||||
break;
|
||||
case State::JUMPING:
|
||||
updateJumping(delta_time);
|
||||
break;
|
||||
case State::FALLING:
|
||||
updateFalling(delta_time);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Actualización lógica del estado ON_GROUND
|
||||
void Player::updateOnGround(float delta_time) {
|
||||
(void)delta_time; // No usado en este método, pero se mantiene por consistencia
|
||||
handleConveyorBelts(); // Gestiona las cintas transportadoras
|
||||
handleShouldFall(); // Verifica si debe caer (no tiene suelo)
|
||||
|
||||
// Verifica si el jugador quiere saltar
|
||||
if (wanna_jump_) { transitionToState(State::JUMPING); }
|
||||
}
|
||||
|
||||
// Actualización lógica del estado ON_SLOPE
|
||||
void Player::updateOnSlope(float delta_time) {
|
||||
(void)delta_time; // No usado en este método, pero se mantiene por consistencia
|
||||
|
||||
handleShouldFall();
|
||||
// NOTA: No llamamos handleShouldFall() aquí porque moveOnSlope() ya maneja
|
||||
// todas las condiciones de salida de la rampa (out of bounds, transición a superficie plana)
|
||||
|
||||
// Verifica si el jugador quiere saltar
|
||||
if (wanna_jump_) { transitionToState(State::JUMPING); }
|
||||
}
|
||||
|
||||
// Actualización lógica del estado JUMPING
|
||||
void Player::updateJumping(float delta_time) {
|
||||
auto_movement_ = false; // Desactiva el movimiento automático durante el salto
|
||||
playJumpSound(delta_time); // Reproduce los sonidos de salto
|
||||
handleJumpEnd(); // Verifica si el salto ha terminado (alcanzó la altura inicial)
|
||||
}
|
||||
|
||||
// Actualización lógica del estado FALLING
|
||||
void Player::updateFalling(float delta_time) {
|
||||
auto_movement_ = false; // Desactiva el movimiento automático durante la caída
|
||||
playFallSound(delta_time); // Reproduce los sonidos de caída
|
||||
}
|
||||
|
||||
// Movimiento físico del estado ON_GROUND
|
||||
void Player::moveOnGround(float delta_time) {
|
||||
// Determinama cuál debe ser la velocidad a partir de automovement o de wanna_go_
|
||||
updateVelocity();
|
||||
|
||||
if (vx_ == 0.0F) { return; }
|
||||
|
||||
// Movimiento horizontal y colision con muros
|
||||
applyHorizontalMovement(delta_time);
|
||||
|
||||
// Comprueba colision con rampas, corrige y cambia estado
|
||||
const int SIDE_X = vx_ < 0.0F ? static_cast<int>(x_) : static_cast<int>(x_) + WIDTH - 1;
|
||||
const LineVertical SIDE = {
|
||||
.x = SIDE_X,
|
||||
.y1 = static_cast<int>(y_) + HEIGHT - 2,
|
||||
.y2 = static_cast<int>(y_) + HEIGHT - 1};
|
||||
|
||||
// Comprueba la rampa correspondiente según la dirección
|
||||
const int SLOPE_Y = vx_ < 0.0F ? room_->checkLeftSlopes(SIDE) : room_->checkRightSlopes(SIDE);
|
||||
if (SLOPE_Y != Collision::NONE) {
|
||||
// Hay rampa: sube al jugador para pegarlo a la rampa
|
||||
y_ = SLOPE_Y - HEIGHT;
|
||||
transitionToState(State::ON_SLOPE);
|
||||
}
|
||||
#ifdef _DEBUG
|
||||
Debug::get()->set("sl.detect_y", SLOPE_Y != Collision::NONE ? std::to_string(SLOPE_Y) : "-");
|
||||
#endif
|
||||
|
||||
// Comprueba si está sobre una rampa
|
||||
if (isOnSlope()) { transitionToState(State::ON_SLOPE); }
|
||||
}
|
||||
|
||||
// Movimiento físico del estado ON_SLOPE
|
||||
void Player::moveOnSlope(float delta_time) {
|
||||
// Determinama cuál debe ser la velocidad a partir de automovement o de wanna_go_
|
||||
updateVelocity();
|
||||
|
||||
// Verificar rampa válida antes de comprobar velocidad: si no hay rampa siempre caer,
|
||||
// independientemente de si hay o no input (evita bloqueo con vx_=0 y slope null)
|
||||
if (current_slope_ == nullptr) {
|
||||
transitionToState(State::FALLING);
|
||||
return;
|
||||
}
|
||||
|
||||
if (vx_ == 0.0F) { return; }
|
||||
|
||||
// Determinar el tipo de rampa
|
||||
const bool IS_LEFT_SLOPE = isLeftSlope();
|
||||
|
||||
// Movimiento horizontal con colisión lateral
|
||||
applyHorizontalMovement(delta_time);
|
||||
|
||||
// Seleccionar el pie apropiado según el tipo de rampa
|
||||
// Left slopes (forma \) colisionan con el pie izquierdo
|
||||
// Right slopes (forma /) colisionan con el pie derecho
|
||||
const int X = IS_LEFT_SLOPE ? x_ : x_ + WIDTH - 1;
|
||||
|
||||
// Calcular la Y basada en la ecuación de la rampa (45 grados)
|
||||
// Left slope (\): y aumenta con x -> y = y1 + (x - x1)
|
||||
// Right slope (/): y disminuye con x -> y = y1 - (x - x1)
|
||||
if (IS_LEFT_SLOPE) {
|
||||
y_ = current_slope_->y1 + (X - current_slope_->x1) - HEIGHT;
|
||||
} else {
|
||||
y_ = current_slope_->y1 - (X - current_slope_->x1) - HEIGHT;
|
||||
}
|
||||
|
||||
// Verificar si el pie ha salido de los límites horizontales de la rampa
|
||||
// Usar min/max porque LEFT slopes tienen x1<x2 pero RIGHT slopes tienen x1>x2
|
||||
const int MIN_X = std::min(current_slope_->x1, current_slope_->x2);
|
||||
const int MAX_X = std::max(current_slope_->x1, current_slope_->x2);
|
||||
const bool OUT_OF_BOUNDS = (X < MIN_X) || (X > MAX_X);
|
||||
|
||||
#ifdef _DEBUG
|
||||
Debug::get()->set("sl.foot", std::to_string(X));
|
||||
Debug::get()->set("sl.y_c", std::to_string(static_cast<int>(y_)));
|
||||
Debug::get()->set("sl.oob", OUT_OF_BOUNDS ? "YES" : "ok");
|
||||
#endif
|
||||
|
||||
if (OUT_OF_BOUNDS) {
|
||||
// Determinar si estamos saliendo por arriba o por abajo de la rampa
|
||||
const bool EXITING_DOWNWARD = (X > current_slope_->x2 && IS_LEFT_SLOPE) ||
|
||||
(X < current_slope_->x1 && !IS_LEFT_SLOPE);
|
||||
const bool EXITING_UPWARD = (X < current_slope_->x1 && IS_LEFT_SLOPE) ||
|
||||
(X > current_slope_->x2 && !IS_LEFT_SLOPE);
|
||||
#ifdef _DEBUG
|
||||
Debug::get()->set("sl.oob", EXITING_DOWNWARD ? "DOWN" : "UP");
|
||||
#endif
|
||||
|
||||
if (EXITING_DOWNWARD) {
|
||||
// Salida por abajo: no hacer nada
|
||||
// y_ += 1.0F;
|
||||
}
|
||||
|
||||
if (EXITING_UPWARD) {
|
||||
// Salida por arriba: bajar un pixel ya que ha subido 1 de mas al salirse de la recta
|
||||
y_ += 1.0F;
|
||||
}
|
||||
|
||||
// Verificar si hay soporte debajo (suelo plano o conveyor belt)
|
||||
if (isOnTopSurface() || isOnConveyorBelt()) {
|
||||
// Hay soporte: transición a ON_GROUND (podría ser superficie o conveyor belt)
|
||||
transitionToState(State::ON_GROUND);
|
||||
} else {
|
||||
// Sin soporte: empezar a caer
|
||||
transitionToState(State::FALLING);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar transición a superficie plana
|
||||
/*if (isOnTopSurface()) {
|
||||
transitionToState(State::ON_GROUND);
|
||||
return;
|
||||
}*/
|
||||
}
|
||||
|
||||
// Movimiento físico del estado JUMPING
|
||||
void Player::moveJumping(float delta_time) {
|
||||
// Movimiento horizontal
|
||||
applyHorizontalMovement(delta_time);
|
||||
|
||||
// Movimiento vertical
|
||||
applyGravity(delta_time);
|
||||
|
||||
const float DISPLACEMENT_Y = vy_ * delta_time;
|
||||
// Movimiento vertical hacia arriba
|
||||
if (vy_ < 0.0F) {
|
||||
const SDL_FRect PROJECTION = getProjection(Direction::UP, DISPLACEMENT_Y);
|
||||
|
||||
// Comprueba la colisión
|
||||
const int POS = room_->checkBottomSurfaces(PROJECTION);
|
||||
|
||||
// Calcula la nueva posición
|
||||
if (POS == Collision::NONE) {
|
||||
// Si no hay colisión
|
||||
y_ += DISPLACEMENT_Y;
|
||||
} else {
|
||||
// Si hay colisión lo mueve hasta donde no colisiona -> FALLING
|
||||
y_ = POS + 1;
|
||||
transitionToState(State::FALLING);
|
||||
}
|
||||
}
|
||||
// Movimiento vertical hacia abajo
|
||||
else if (vy_ > 0.0F) {
|
||||
// Crea el rectangulo de proyección en el eje Y para ver si colisiona
|
||||
const SDL_FRect PROJECTION = getProjection(Direction::DOWN, DISPLACEMENT_Y);
|
||||
|
||||
// JUMPING colisiona con rampas solo si vx_ == 0
|
||||
if (vx_ == 0.0F) {
|
||||
handleLandingFromAir(DISPLACEMENT_Y, PROJECTION);
|
||||
} else {
|
||||
// Comprueba la colisión con las superficies y las cintas transportadoras (sin rampas)
|
||||
// Extendemos 1px hacia arriba para detectar suelos traversados ligeramente al
|
||||
// entrar horizontalmente (consecuencia del margen h=HEIGHT-1 en la proyección horizontal)
|
||||
const SDL_FRect ADJ = {.x = PROJECTION.x, .y = PROJECTION.y - 1.0F, .w = PROJECTION.w, .h = PROJECTION.h + 1.0F};
|
||||
const float POS = std::max(room_->checkTopSurfaces(ADJ), room_->checkAutoSurfaces(ADJ));
|
||||
if (POS != Collision::NONE) {
|
||||
// Si hay colisión lo mueve hasta donde no colisiona y pasa a estar sobre la superficie
|
||||
y_ = POS - HEIGHT;
|
||||
transitionToState(State::ON_GROUND);
|
||||
} else {
|
||||
// Esta saltando con movimiento horizontal y no hay colisión con los muros
|
||||
// Calcula la nueva posición (atraviesa rampas)
|
||||
y_ += DISPLACEMENT_Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Movimiento físico del estado FALLING
|
||||
void Player::moveFalling(float delta_time) {
|
||||
// Crea el rectangulo de proyección en el eje Y para ver si colisiona
|
||||
const float DISPLACEMENT = vy_ * delta_time;
|
||||
const SDL_FRect PROJECTION = getProjection(Direction::DOWN, DISPLACEMENT);
|
||||
|
||||
// Comprueba aterrizaje en superficies y rampas
|
||||
handleLandingFromAir(DISPLACEMENT, PROJECTION);
|
||||
}
|
||||
|
||||
// Comprueba si está situado en alguno de los cuatro bordes de la habitación
|
||||
auto Player::handleBorders() -> Room::Border {
|
||||
if (x_ < PlayArea::LEFT) {
|
||||
return Room::Border::LEFT;
|
||||
}
|
||||
|
||||
if (x_ + WIDTH > PlayArea::RIGHT) {
|
||||
return Room::Border::RIGHT;
|
||||
}
|
||||
|
||||
if (y_ < PlayArea::TOP) {
|
||||
return Room::Border::TOP;
|
||||
}
|
||||
|
||||
if (y_ + HEIGHT > PlayArea::BOTTOM) {
|
||||
// Si llega en estado terminal, muere y no cruza
|
||||
const bool SHOULD_DIE = static_cast<int>(y_) - last_grounded_position_ > MAX_FALLING_HEIGHT;
|
||||
if (SHOULD_DIE) { markAsDead(); }
|
||||
return is_alive_ ? Room::Border::BOTTOM : Room::Border::NONE;
|
||||
}
|
||||
|
||||
return Room::Border::NONE;
|
||||
}
|
||||
|
||||
// Cambia al jugador de un borde al opuesto. Util para el cambio de pantalla
|
||||
void Player::switchBorders() {
|
||||
switch (border_) {
|
||||
case Room::Border::TOP:
|
||||
y_ = PlayArea::BOTTOM - HEIGHT - Tile::SIZE;
|
||||
// CRÍTICO: Resetear last_grounded_position_ para evitar muerte falsa por diferencia de Y entre pantallas
|
||||
last_grounded_position_ = static_cast<int>(y_);
|
||||
transitionToState(State::ON_GROUND); // TODO: Detectar si debe ser ON_SLOPE
|
||||
break;
|
||||
|
||||
case Room::Border::BOTTOM:
|
||||
y_ = PlayArea::TOP;
|
||||
// CRÍTICO: Resetear last_grounded_position_ para evitar muerte falsa por diferencia de Y entre pantallas
|
||||
last_grounded_position_ = static_cast<int>(y_);
|
||||
transitionToState(State::ON_GROUND); // TODO: Detectar si debe ser ON_SLOPE
|
||||
break;
|
||||
|
||||
case Room::Border::RIGHT:
|
||||
x_ = PlayArea::LEFT;
|
||||
break;
|
||||
|
||||
case Room::Border::LEFT:
|
||||
x_ = PlayArea::RIGHT - WIDTH;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
border_ = Room::Border::NONE;
|
||||
syncSpriteAndCollider();
|
||||
}
|
||||
|
||||
// Aplica gravedad al jugador
|
||||
void Player::applyGravity(float delta_time) {
|
||||
// La gravedad solo se aplica cuando el jugador esta saltando
|
||||
// Nunca mientras cae o esta de pie
|
||||
if (state_ == State::JUMPING) {
|
||||
vy_ += GRAVITY_FORCE * delta_time;
|
||||
vy_ = std::min(vy_, MAX_VY);
|
||||
}
|
||||
}
|
||||
|
||||
// Establece la animación del jugador
|
||||
void Player::animate(float delta_time) { // NOLINT(readability-make-member-function-const)
|
||||
if (vx_ != 0) {
|
||||
sprite_->update(delta_time);
|
||||
}
|
||||
}
|
||||
|
||||
// Comprueba si ha finalizado el salto al alcanzar la altura de inicio
|
||||
void Player::handleJumpEnd() {
|
||||
// Si el jugador vuelve EXACTAMENTE a la altura inicial, debe CONTINUAR en JUMPING
|
||||
// Solo cuando la SUPERA (desciende más allá) cambia a FALLING
|
||||
if (state_ == State::JUMPING && vy_ > 0.0F && static_cast<int>(y_) > last_grounded_position_) {
|
||||
transitionToState(State::FALLING);
|
||||
}
|
||||
}
|
||||
|
||||
// Calcula y reproduce el sonido de salto basado en tiempo transcurrido
|
||||
void Player::playJumpSound(float delta_time) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
size_t sound_index;
|
||||
if (jump_sound_ctrl_.shouldPlay(delta_time, sound_index)) {
|
||||
if (sound_index < jumping_sound_.size()) {
|
||||
Audio::get()->playSound(jumping_sound_[sound_index], Audio::Group::GAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calcula y reproduce el sonido de caída basado en distancia vertical recorrida
|
||||
void Player::playFallSound(float delta_time) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
size_t sound_index;
|
||||
if (fall_sound_ctrl_.shouldPlay(delta_time, y_, sound_index)) {
|
||||
if (sound_index < falling_sound_.size()) {
|
||||
Audio::get()->playSound(falling_sound_[sound_index], Audio::Group::GAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Comprueba si el jugador tiene suelo debajo de los pies
|
||||
auto Player::isOnFloor() -> bool {
|
||||
bool on_top_surface = false;
|
||||
bool on_conveyor_belt = false;
|
||||
updateFeet();
|
||||
|
||||
// Comprueba las superficies
|
||||
on_top_surface |= room_->checkTopSurfaces(under_left_foot_);
|
||||
on_top_surface |= room_->checkTopSurfaces(under_right_foot_);
|
||||
|
||||
// Comprueba las cintas transportadoras
|
||||
on_conveyor_belt |= room_->checkConveyorBelts(under_left_foot_);
|
||||
on_conveyor_belt |= room_->checkConveyorBelts(under_right_foot_);
|
||||
|
||||
// Comprueba las rampas
|
||||
auto on_slope_l = room_->checkLeftSlopes(under_left_foot_);
|
||||
auto on_slope_r = room_->checkRightSlopes(under_right_foot_);
|
||||
|
||||
return on_top_surface || on_conveyor_belt || on_slope_l || on_slope_r;
|
||||
}
|
||||
|
||||
// Comprueba si el jugador está sobre una superficie
|
||||
auto Player::isOnTopSurface() -> bool {
|
||||
bool on_top_surface = false;
|
||||
updateFeet();
|
||||
|
||||
// Comprueba las superficies
|
||||
on_top_surface |= room_->checkTopSurfaces(under_left_foot_);
|
||||
on_top_surface |= room_->checkTopSurfaces(under_right_foot_);
|
||||
|
||||
return on_top_surface;
|
||||
}
|
||||
|
||||
// Comprueba si el jugador esta sobre una cinta transportadora
|
||||
auto Player::isOnConveyorBelt() -> bool {
|
||||
bool on_conveyor_belt = false;
|
||||
updateFeet();
|
||||
|
||||
// Comprueba las superficies
|
||||
on_conveyor_belt |= room_->checkConveyorBelts(under_left_foot_);
|
||||
on_conveyor_belt |= room_->checkConveyorBelts(under_right_foot_);
|
||||
|
||||
return on_conveyor_belt;
|
||||
}
|
||||
|
||||
// Comprueba si el jugador está sobre una rampa
|
||||
// Retorna true SOLO si un pie está en rampa Y el otro pie está volando (sin soporte)
|
||||
auto Player::isOnSlope() -> bool {
|
||||
updateFeet();
|
||||
|
||||
// Verificar qué pie está en qué tipo de rampa
|
||||
const bool LEFT_FOOT_ON_LEFT_SLOPE = room_->checkLeftSlopes(under_left_foot_);
|
||||
const bool RIGHT_FOOT_ON_RIGHT_SLOPE = room_->checkRightSlopes(under_right_foot_);
|
||||
|
||||
// Verificar si cada pie está "volando" (sin soporte: ni top surface ni conveyor belt)
|
||||
const bool LEFT_FOOT_FLYING = !(room_->checkTopSurfaces(under_left_foot_) ||
|
||||
room_->checkConveyorBelts(under_left_foot_));
|
||||
const bool RIGHT_FOOT_FLYING = !(room_->checkTopSurfaces(under_right_foot_) ||
|
||||
room_->checkConveyorBelts(under_right_foot_));
|
||||
|
||||
// Retornar true si UN pie en rampa Y el OTRO volando
|
||||
return (LEFT_FOOT_ON_LEFT_SLOPE && RIGHT_FOOT_FLYING) ||
|
||||
(RIGHT_FOOT_ON_RIGHT_SLOPE && LEFT_FOOT_FLYING);
|
||||
}
|
||||
|
||||
// Comprueba si current_slope_ es una rampa izquierda (ascendente a la izquierda)
|
||||
// Las rampas izquierdas tienen forma \ con x1 < x2 (x aumenta de izq a der)
|
||||
auto Player::isLeftSlope() -> bool {
|
||||
if (current_slope_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
// Left slopes (\): x1 < x2 (x aumenta de izquierda a derecha)
|
||||
// Right slopes (/): x1 > x2 (x decrece de izquierda a derecha)
|
||||
return current_slope_->x1 < current_slope_->x2;
|
||||
}
|
||||
|
||||
// Actualiza current_slope_ con la rampa correcta según el pie que toca
|
||||
void Player::updateCurrentSlope() {
|
||||
updateFeet();
|
||||
|
||||
// Left slopes (\) ascendentes a izquierda tocan el pie izquierdo
|
||||
if (room_->checkLeftSlopes(under_left_foot_)) {
|
||||
current_slope_ = room_->getSlopeAtPoint(under_left_foot_);
|
||||
}
|
||||
// Right slopes (/) ascendentes a derecha tocan el pie derecho
|
||||
else if (room_->checkRightSlopes(under_right_foot_)) {
|
||||
current_slope_ = room_->getSlopeAtPoint(under_right_foot_);
|
||||
}
|
||||
// Fallback para casos edge
|
||||
else {
|
||||
current_slope_ = room_->getSlopeAtPoint(under_left_foot_);
|
||||
if (current_slope_ == nullptr) {
|
||||
current_slope_ = room_->getSlopeAtPoint(under_right_foot_);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
if (current_slope_ != nullptr) {
|
||||
Debug::get()->set("sl.type", isLeftSlope() ? "L\\" : "R/");
|
||||
Debug::get()->set("sl.p1", std::to_string(current_slope_->x1) + "," + std::to_string(current_slope_->y1));
|
||||
Debug::get()->set("sl.p2", std::to_string(current_slope_->x2) + "," + std::to_string(current_slope_->y2));
|
||||
} else {
|
||||
Debug::get()->set("sl.type", "null");
|
||||
Debug::get()->unset("sl.p1");
|
||||
Debug::get()->unset("sl.p2");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Comprueba que el jugador no toque ningun tile de los que matan
|
||||
auto Player::handleKillingTiles() -> bool {
|
||||
// Comprueba si hay contacto con algún tile que mata
|
||||
if (std::ranges::any_of(collider_points_, [this](const auto& c) -> bool {
|
||||
return room_->getTile(c) == Room::Tile::KILL;
|
||||
})) {
|
||||
markAsDead(); // Mata al jugador inmediatamente
|
||||
return true; // Retorna en cuanto se detecta una colisión
|
||||
}
|
||||
|
||||
return false; // No se encontró ninguna colisión
|
||||
}
|
||||
|
||||
// Establece el color del jugador (0 = automático según options)
|
||||
void Player::setColor(Uint8 color) {
|
||||
if (color != 0) {
|
||||
color_ = color;
|
||||
return;
|
||||
}
|
||||
|
||||
// Color personalizado desde opciones
|
||||
if (Options::game.player_color >= 0) {
|
||||
color_ = static_cast<Uint8>(Options::game.player_color);
|
||||
} else {
|
||||
color_ = static_cast<Uint8>(PaletteColor::WHITE);
|
||||
}
|
||||
|
||||
// Si el color coincide con el fondo de la habitación, usar fallback
|
||||
if (room_ != nullptr && color_ == room_->getBGColor()) {
|
||||
color_ = (room_->getBGColor() != static_cast<Uint8>(PaletteColor::WHITE))
|
||||
? static_cast<Uint8>(PaletteColor::WHITE)
|
||||
: static_cast<Uint8>(PaletteColor::BRIGHT_BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
// Actualiza los puntos de colisión
|
||||
void Player::updateColliderPoints() {
|
||||
const SDL_FRect RECT = getRect();
|
||||
collider_points_[0] = {.x = RECT.x, .y = RECT.y};
|
||||
collider_points_[1] = {.x = RECT.x + 7, .y = RECT.y};
|
||||
collider_points_[2] = {.x = RECT.x + 7, .y = RECT.y + 7};
|
||||
collider_points_[3] = {.x = RECT.x, .y = RECT.y + 7};
|
||||
collider_points_[4] = {.x = RECT.x, .y = RECT.y + 8};
|
||||
collider_points_[5] = {.x = RECT.x + 7, .y = RECT.y + 8};
|
||||
collider_points_[6] = {.x = RECT.x + 7, .y = RECT.y + 15};
|
||||
collider_points_[7] = {.x = RECT.x, .y = RECT.y + 15};
|
||||
}
|
||||
|
||||
// Actualiza los puntos de los pies
|
||||
void Player::updateFeet() {
|
||||
under_left_foot_ = {
|
||||
.x = x_,
|
||||
.y = y_ + HEIGHT};
|
||||
under_right_foot_ = {
|
||||
.x = x_ + WIDTH - 1,
|
||||
.y = y_ + HEIGHT};
|
||||
}
|
||||
|
||||
// Inicializa los sonidos de salto y caida
|
||||
void Player::initSounds() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
for (int i = 0; i < 24; ++i) {
|
||||
std::string sound_file = "jump" + std::to_string(i + 1) + ".wav";
|
||||
jumping_sound_[i] = Resource::Cache::get()->getSound(sound_file);
|
||||
|
||||
if (i >= 10) { // i+1 >= 11
|
||||
falling_sound_[i - 10] = Resource::Cache::get()->getSound(sound_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implementación de JumpSoundController::start
|
||||
void Player::JumpSoundController::start() {
|
||||
current_index = 0;
|
||||
elapsed_time = 0.0F;
|
||||
active = true;
|
||||
}
|
||||
|
||||
// Implementación de JumpSoundController::reset
|
||||
void Player::JumpSoundController::reset() {
|
||||
active = false;
|
||||
current_index = 0;
|
||||
elapsed_time = 0.0F;
|
||||
}
|
||||
|
||||
// Implementación de JumpSoundController::shouldPlay
|
||||
auto Player::JumpSoundController::shouldPlay(float delta_time, size_t& out_index) -> bool {
|
||||
if (!active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Acumula el tiempo transcurrido durante el salto
|
||||
elapsed_time += delta_time;
|
||||
|
||||
// Calcula qué sonido debería estar sonando según el tiempo
|
||||
size_t target_index = FIRST_SOUND + static_cast<size_t>((elapsed_time / SECONDS_PER_SOUND));
|
||||
target_index = std::min(target_index, LAST_SOUND);
|
||||
|
||||
// Reproduce si hemos avanzado a un nuevo sonido
|
||||
if (target_index > current_index) {
|
||||
current_index = target_index;
|
||||
out_index = current_index;
|
||||
return true; // NOLINT(readability-simplify-boolean-expr)
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Implementación de FallSoundController::start
|
||||
void Player::FallSoundController::start(float start_y) {
|
||||
current_index = 0;
|
||||
distance_traveled = 0.0F;
|
||||
last_y = start_y;
|
||||
active = true;
|
||||
}
|
||||
|
||||
// Implementación de FallSoundController::reset
|
||||
void Player::FallSoundController::reset() {
|
||||
active = false;
|
||||
current_index = 0;
|
||||
distance_traveled = 0.0F;
|
||||
}
|
||||
|
||||
// Implementación de FallSoundController::shouldPlay
|
||||
auto Player::FallSoundController::shouldPlay(float delta_time, float current_y, size_t& out_index) -> bool {
|
||||
(void)delta_time; // No usado actualmente, pero recibido por consistencia
|
||||
|
||||
if (!active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Acumula la distancia recorrida (solo hacia abajo)
|
||||
if (current_y > last_y) {
|
||||
distance_traveled += (current_y - last_y);
|
||||
}
|
||||
last_y = current_y;
|
||||
|
||||
// Calcula qué sonido debería estar sonando según el intervalo
|
||||
size_t target_index = FIRST_SOUND + static_cast<size_t>((distance_traveled / PIXELS_PER_SOUND));
|
||||
|
||||
// El sonido a reproducir se limita a LAST_SOUND (13), pero el índice interno sigue creciendo
|
||||
size_t sound_to_play = std::min(target_index, LAST_SOUND);
|
||||
|
||||
// Reproduce si hemos avanzado a un nuevo índice (permite repetición de sonido 13)
|
||||
if (target_index > current_index) {
|
||||
current_index = target_index; // Guardamos el índice real (puede ser > LAST_SOUND)
|
||||
out_index = sound_to_play; // Pero reproducimos LAST_SOUND cuando corresponde
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Aplica los valores de spawn al jugador
|
||||
void Player::applySpawnValues(const SpawnData& spawn) {
|
||||
x_ = spawn.x;
|
||||
y_ = spawn.y;
|
||||
y_prev_ = spawn.y; // Inicializar y_prev_ igual a y_ para evitar saltos en primer frame
|
||||
vx_ = spawn.vx;
|
||||
vy_ = spawn.vy;
|
||||
last_grounded_position_ = spawn.last_grounded_position;
|
||||
state_ = spawn.state;
|
||||
sprite_->setFlip(spawn.flip);
|
||||
}
|
||||
|
||||
// Resuelve nombre de skin a fichero de animación
|
||||
auto Player::skinToAnimationPath(const std::string& skin_name) -> std::string {
|
||||
if (skin_name == "default") {
|
||||
return "player.yaml";
|
||||
}
|
||||
return skin_name + ".yaml";
|
||||
}
|
||||
|
||||
// Cambia la skin del jugador en caliente preservando la orientación actual
|
||||
void Player::setSkin(const std::string& skin_name) {
|
||||
const auto FLIP = sprite_->getFlip();
|
||||
initSprite(skinToAnimationPath(skin_name));
|
||||
sprite_->setFlip(FLIP);
|
||||
}
|
||||
|
||||
// Inicializa el sprite del jugador
|
||||
void Player::initSprite(const std::string& animations_path) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const auto& animation_data = Resource::Cache::get()->getAnimationData(animations_path);
|
||||
sprite_ = std::make_unique<AnimatedSprite>(animation_data);
|
||||
sprite_->setWidth(WIDTH);
|
||||
sprite_->setHeight(HEIGHT);
|
||||
sprite_->setCurrentAnimation("default");
|
||||
}
|
||||
|
||||
// Actualiza la posición del sprite y las colisiones
|
||||
void Player::syncSpriteAndCollider() {
|
||||
placeSprite(); // Coloca el sprite en la posición del jugador
|
||||
collider_box_ = getRect(); // Actualiza el rectangulo de colisión
|
||||
updateColliderPoints(); // Actualiza los puntos de colisión
|
||||
#ifdef _DEBUG
|
||||
updateFeet();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Coloca el sprite en la posición del jugador
|
||||
void Player::placeSprite() {
|
||||
sprite_->setPos(x_, y_);
|
||||
}
|
||||
|
||||
// Gestiona la muerta al ccaer desde muy alto
|
||||
void Player::handleDeathByFalling() {
|
||||
const int FALL_DISTANCE = static_cast<int>(y_) - last_grounded_position_;
|
||||
if (previous_state_ == State::FALLING && FALL_DISTANCE > MAX_FALLING_HEIGHT) {
|
||||
markAsDead(); // Muere si cae más de 32 píxeles
|
||||
}
|
||||
}
|
||||
|
||||
// Calcula la velocidad en x
|
||||
void Player::updateVelocity() {
|
||||
if (auto_movement_) {
|
||||
// La cinta transportadora tiene el control
|
||||
vx_ = HORIZONTAL_VELOCITY * room_->getConveyorBeltDirection();
|
||||
sprite_->setFlip(vx_ < 0.0F ? Flip::LEFT : Flip::RIGHT);
|
||||
} else {
|
||||
// El jugador tiene el control
|
||||
switch (wanna_go_) {
|
||||
case Direction::LEFT:
|
||||
vx_ = -HORIZONTAL_VELOCITY;
|
||||
sprite_->setFlip(Flip::LEFT);
|
||||
break;
|
||||
case Direction::RIGHT:
|
||||
vx_ = HORIZONTAL_VELOCITY;
|
||||
sprite_->setFlip(Flip::RIGHT);
|
||||
break;
|
||||
case Direction::NONE:
|
||||
vx_ = 0.0F;
|
||||
break;
|
||||
default:
|
||||
vx_ = 0.0F;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aplica movimiento horizontal con colisión de muros
|
||||
void Player::applyHorizontalMovement(float delta_time) {
|
||||
if (vx_ == 0.0F) { return; }
|
||||
|
||||
const float DISPLACEMENT = vx_ * delta_time;
|
||||
if (vx_ < 0.0F) {
|
||||
const SDL_FRect PROJECTION = getProjection(Direction::LEFT, DISPLACEMENT);
|
||||
const int POS = room_->checkRightSurfaces(PROJECTION);
|
||||
if (POS == Collision::NONE) {
|
||||
x_ += DISPLACEMENT;
|
||||
} else {
|
||||
x_ = POS + 1;
|
||||
}
|
||||
} else {
|
||||
const SDL_FRect PROJECTION = getProjection(Direction::RIGHT, DISPLACEMENT);
|
||||
const int POS = room_->checkLeftSurfaces(PROJECTION);
|
||||
if (POS == Collision::NONE) {
|
||||
x_ += DISPLACEMENT;
|
||||
} else {
|
||||
x_ = POS - WIDTH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detecta aterrizaje en superficies y rampas
|
||||
auto Player::handleLandingFromAir(float displacement, const SDL_FRect& projection) -> bool {
|
||||
// Comprueba la colisión con las superficies y las cintas transportadoras
|
||||
const float POS = std::max(room_->checkTopSurfaces(projection), room_->checkAutoSurfaces(projection));
|
||||
if (POS != Collision::NONE) {
|
||||
// Si hay colisión lo mueve hasta donde no colisiona y pasa a estar sobre la superficie
|
||||
y_ = POS - HEIGHT;
|
||||
transitionToState(State::ON_GROUND);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Comprueba la colisión con las rampas
|
||||
auto rect = toSDLRect(projection);
|
||||
const LineVertical LEFT_SIDE = {.x = rect.x, .y1 = rect.y, .y2 = rect.y + rect.h};
|
||||
const LineVertical RIGHT_SIDE = {.x = rect.x + rect.w - 1, .y1 = rect.y, .y2 = rect.y + rect.h};
|
||||
const float POINT = std::max(room_->checkRightSlopes(RIGHT_SIDE), room_->checkLeftSlopes(LEFT_SIDE));
|
||||
if (POINT != Collision::NONE) {
|
||||
y_ = POINT - HEIGHT;
|
||||
transitionToState(State::ON_SLOPE);
|
||||
return true;
|
||||
}
|
||||
|
||||
// No hay colisión
|
||||
y_ += displacement;
|
||||
#ifdef _DEBUG
|
||||
// Guarda por si en debug el jugador se sale de la pantalla, para que no esté cayendo infinitamente
|
||||
if (y_ > PlayArea::BOTTOM + HEIGHT) { y_ = PlayArea::TOP + 2; }
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resetea los controladores de sonido al aterrizar
|
||||
void Player::resetSoundControllersOnLanding() {
|
||||
jump_sound_ctrl_.reset();
|
||||
fall_sound_ctrl_.reset();
|
||||
}
|
||||
|
||||
// Devuelve el rectangulo de proyeccion
|
||||
auto Player::getProjection(Direction direction, float displacement) -> SDL_FRect { // NOLINT(readability-convert-member-functions-to-static)
|
||||
switch (direction) {
|
||||
case Direction::LEFT:
|
||||
return {
|
||||
.x = x_ + displacement,
|
||||
.y = y_,
|
||||
.w = std::ceil(std::fabs(displacement)), // Para evitar que tenga una anchura de 0 pixels
|
||||
.h = HEIGHT - 1}; // -1 para dar ventana de 2px en aperturas de altura exacta
|
||||
|
||||
case Direction::RIGHT:
|
||||
return {
|
||||
.x = x_ + WIDTH,
|
||||
.y = y_,
|
||||
.w = std::ceil(displacement), // Para evitar que tenga una anchura de 0 pixels
|
||||
.h = HEIGHT - 1}; // -1 para dar ventana de 2px en aperturas de altura exacta
|
||||
|
||||
case Direction::UP:
|
||||
return {
|
||||
.x = x_,
|
||||
.y = y_ + displacement,
|
||||
.w = WIDTH,
|
||||
.h = std::ceil(std::fabs(displacement)) // Para evitar que tenga una altura de 0 pixels
|
||||
};
|
||||
|
||||
case Direction::DOWN:
|
||||
return {
|
||||
.x = x_,
|
||||
.y = y_ + HEIGHT,
|
||||
.w = WIDTH,
|
||||
.h = std::ceil(displacement) // Para evitar que tenga una altura de 0 pixels
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
.x = 0.0F,
|
||||
.y = 0.0F,
|
||||
.w = 0.0F,
|
||||
.h = 0.0F};
|
||||
}
|
||||
}
|
||||
|
||||
// Marca al jugador como muerto
|
||||
void Player::markAsDead() {
|
||||
is_alive_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED);
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Establece la posición del jugador directamente (debug)
|
||||
void Player::setDebugPosition(float x, float y) {
|
||||
x_ = x;
|
||||
y_ = y;
|
||||
syncSpriteAndCollider();
|
||||
}
|
||||
|
||||
// Fija estado ON_GROUND, velocidades a 0, actualiza last_grounded_position_ (debug)
|
||||
void Player::finalizeDebugTeleport() {
|
||||
vx_ = 0.0F;
|
||||
vy_ = 0.0F;
|
||||
last_grounded_position_ = static_cast<int>(y_);
|
||||
transitionToState(State::ON_GROUND);
|
||||
syncSpriteAndCollider();
|
||||
}
|
||||
#endif
|
||||
226
source/game/entities/player.hpp
Normal file
226
source/game/entities/player.hpp
Normal file
@@ -0,0 +1,226 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array> // Para array
|
||||
#include <limits> // Para numeric_limits
|
||||
#include <memory> // Para shared_ptr, __shared_ptr_access
|
||||
#include <string> // Para string
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/sprite/animated_sprite.hpp" // Para SAnimatedSprite
|
||||
#include "game/gameplay/room.hpp"
|
||||
#include "game/options.hpp" // Para Cheat, Options, options
|
||||
#include "utils/defines.hpp" // Para BORDER_TOP, BLOCK
|
||||
#include "utils/utils.hpp" // Para Color
|
||||
struct JA_Sound_t; // lines 13-13
|
||||
|
||||
class Player {
|
||||
public:
|
||||
// --- Enums y Structs ---
|
||||
enum class State {
|
||||
ON_GROUND, // En suelo plano o conveyor belt
|
||||
ON_SLOPE, // En rampa/pendiente
|
||||
JUMPING,
|
||||
FALLING,
|
||||
};
|
||||
|
||||
enum class Direction {
|
||||
LEFT,
|
||||
RIGHT,
|
||||
UP,
|
||||
DOWN,
|
||||
NONE
|
||||
};
|
||||
|
||||
// --- Constantes de física (públicas para permitir cálculos en structs) ---
|
||||
static constexpr float HORIZONTAL_VELOCITY = 40.0F; // Velocidad horizontal en pixels/segundo (0.6 * 66.67fps)
|
||||
static constexpr float MAX_VY = 80.0F; // Velocidad vertical máxima en pixels/segundo (1.2 * 66.67fps)
|
||||
static constexpr float JUMP_VELOCITY = -80.0F; // Velocidad inicial del salto en pixels/segundo
|
||||
static constexpr float GRAVITY_FORCE = 155.6F; // Fuerza de gravedad en pixels/segundo² (0.035 * 66.67²)
|
||||
|
||||
struct SpawnData {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
float vx = 0;
|
||||
float vy = 0;
|
||||
int last_grounded_position = 0;
|
||||
State state = State::ON_GROUND;
|
||||
SDL_FlipMode flip = SDL_FLIP_NONE;
|
||||
};
|
||||
|
||||
struct Data {
|
||||
SpawnData spawn_data;
|
||||
std::string animations_path;
|
||||
std::shared_ptr<Room> room = nullptr;
|
||||
};
|
||||
|
||||
struct JumpSoundController {
|
||||
// Duración del salto calculada automáticamente con física: t = 2 * v0 / g
|
||||
static constexpr float JUMP_DURATION = (2.0F * MAX_VY) / GRAVITY_FORCE;
|
||||
static constexpr size_t FIRST_SOUND = 1; // Primer sonido a reproducir (índice 1)
|
||||
static constexpr size_t LAST_SOUND = 17; // Último sonido a reproducir (índice 17)
|
||||
static constexpr float SECONDS_PER_SOUND = JUMP_DURATION / (LAST_SOUND - FIRST_SOUND + 1);
|
||||
|
||||
size_t current_index = 0; // Índice del sonido actual
|
||||
float elapsed_time = 0.0F; // Tiempo transcurrido durante el salto
|
||||
bool active = false; // Indica si el controlador está activo
|
||||
|
||||
void start(); // Inicia el controlador
|
||||
void reset(); // Resetea el controlador
|
||||
auto shouldPlay(float delta_time, size_t& out_index) -> bool; // Comprueba si debe reproducir un sonido
|
||||
};
|
||||
|
||||
struct FallSoundController {
|
||||
static constexpr float PIXELS_PER_SOUND = 5.0F; // Intervalo de píxeles por sonido (configurable)
|
||||
static constexpr size_t FIRST_SOUND = 1; // Primer sonido a reproducir (índice 1)
|
||||
static constexpr size_t LAST_SOUND = 13; // Último sonido a reproducir (índice 13)
|
||||
|
||||
size_t current_index = 0; // Índice del sonido actual
|
||||
float distance_traveled = 0.0F; // Distancia acumulada durante la caída
|
||||
float last_y = 0.0F; // Última posición Y registrada
|
||||
bool active = false; // Indica si el controlador está activo
|
||||
|
||||
void start(float start_y); // Inicia el controlador
|
||||
void reset(); // Resetea el controlador
|
||||
auto shouldPlay(float delta_time, float current_y, size_t& out_index) -> bool; // Comprueba si debe reproducir un sonido
|
||||
};
|
||||
|
||||
// --- Constructor y Destructor ---
|
||||
explicit Player(const Data& player);
|
||||
~Player() = default;
|
||||
|
||||
// --- Funciones ---
|
||||
void render(); // Pinta el enemigo en pantalla
|
||||
void update(float delta_time); // Actualiza las variables del objeto
|
||||
[[nodiscard]] auto isOnBorder() const -> bool { return border_ != Room::Border::NONE; } // Indica si el jugador esta en uno de los cuatro bordes de la pantalla
|
||||
[[nodiscard]] auto getBorder() const -> Room::Border { return border_; } // Indica en cual de los cuatro bordes se encuentra
|
||||
void switchBorders(); // Cambia al jugador de un borde al opuesto. Util para el cambio de pantalla
|
||||
auto getRect() -> SDL_FRect { return {.x = x_, .y = y_, .w = WIDTH, .h = HEIGHT}; } // Obtiene el rectangulo que delimita al jugador
|
||||
auto getCollider() -> SDL_FRect& { return collider_box_; } // Obtiene el rectangulo de colision del jugador
|
||||
auto getSpawnParams() -> SpawnData { return {.x = x_, .y = y_, .vx = vx_, .vy = vy_, .last_grounded_position = last_grounded_position_, .state = state_, .flip = sprite_->getFlip()}; } // Obtiene el estado de reaparición del jugador
|
||||
void setColor(Uint8 color = 0); // Establece el color del jugador (0 = automático según cheats)
|
||||
void setSkin(const std::string& skin_name); // Cambia la skin del jugador en caliente ("default" o nombre de enemigo)
|
||||
static auto skinToAnimationPath(const std::string& skin_name) -> std::string; // Resuelve nombre de skin a fichero de animación
|
||||
void setRoom(std::shared_ptr<Room> room) { room_ = std::move(room); } // Establece la habitación en la que se encuentra el jugador
|
||||
//[[nodiscard]] auto isAlive() const -> bool { return is_alive_ || (Options::cheats.invincible == Options::Cheat::State::ENABLED); } // Comprueba si el jugador esta vivo
|
||||
[[nodiscard]] auto isAlive() const -> bool { return is_alive_; } // Comprueba si el jugador esta vivo
|
||||
void setPaused(bool value) { is_paused_ = value; } // Pone el jugador en modo pausa
|
||||
void setIgnoreInput(bool value) { ignore_input_ = value; } // Ignora inputs del jugador (física sigue activa)
|
||||
[[nodiscard]] auto getIgnoreInput() const -> bool { return ignore_input_; }
|
||||
|
||||
#ifdef _DEBUG
|
||||
// --- Funciones de debug ---
|
||||
void setDebugPosition(float x, float y); // Establece la posición del jugador directamente (debug)
|
||||
void finalizeDebugTeleport(); // Fija estado ON_GROUND, velocidades a 0, actualiza last_grounded_position_ (debug)
|
||||
#endif
|
||||
|
||||
private:
|
||||
// --- Constantes ---
|
||||
static constexpr int WIDTH = 8; // Ancho del jugador
|
||||
static constexpr int HEIGHT = 16; // ALto del jugador
|
||||
static constexpr int MAX_FALLING_HEIGHT = Tile::SIZE * 4; // Altura maxima permitida de caída en pixels
|
||||
|
||||
// --- Objetos y punteros ---
|
||||
std::shared_ptr<Room> room_; // Objeto encargado de gestionar cada habitación del juego
|
||||
std::unique_ptr<AnimatedSprite> sprite_; // Sprite del jugador
|
||||
|
||||
// --- Variables de posición y física ---
|
||||
float x_ = 0.0F; // Posición del jugador en el eje X
|
||||
float y_ = 0.0F; // Posición del jugador en el eje Y
|
||||
float y_prev_ = 0.0F; // Posición Y del frame anterior (para detectar hitos de distancia en sonidos)
|
||||
float vx_ = 0.0F; // Velocidad/desplazamiento del jugador en el eje X
|
||||
float vy_ = 0.0F; // Velocidad/desplazamiento del jugador en el eje Y
|
||||
|
||||
Direction wanna_go_ = Direction::NONE;
|
||||
bool wanna_jump_ = false;
|
||||
|
||||
// --- Variables de estado ---
|
||||
State state_ = State::ON_GROUND; // Estado en el que se encuentra el jugador. Util apara saber si está saltando o cayendo
|
||||
State previous_state_ = State::ON_GROUND; // Estado previo en el que se encontraba el jugador
|
||||
|
||||
// --- Variables de colisión ---
|
||||
SDL_FRect collider_box_{}; // Caja de colisión con los enemigos u objetos
|
||||
std::array<SDL_FPoint, 8> collider_points_{}; // Puntos de colisión con el mapa
|
||||
SDL_FPoint under_left_foot_ = {.x = 0.0F, .y = 0.0F}; // El punto bajo la esquina inferior izquierda del jugador
|
||||
SDL_FPoint under_right_foot_ = {.x = 0.0F, .y = 0.0F}; // El punto bajo la esquina inferior derecha del jugador
|
||||
const LineDiagonal* current_slope_{nullptr}; // Rampa actual sobe la que está el jugador
|
||||
|
||||
// --- Variables de juego ---
|
||||
bool is_alive_ = true; // Indica si el jugador esta vivo o no
|
||||
bool is_paused_ = false; // Indica si el jugador esta en modo pausa
|
||||
bool ignore_input_ = false; // Ignora inputs pero mantiene la física activa
|
||||
bool auto_movement_ = false; // Indica si esta siendo arrastrado por una superficie automatica
|
||||
Room::Border border_ = Room::Border::TOP; // Indica en cual de los cuatro bordes se encuentra
|
||||
int last_grounded_position_ = 0; // Ultima posición en Y en la que se estaba en contacto con el suelo (hace doble función: tracking de caída + altura inicial del salto)
|
||||
|
||||
// --- Variables de renderizado y sonido ---
|
||||
Uint8 color_ = 0; // Color del jugador
|
||||
std::array<JA_Sound_t*, 24> jumping_sound_{}; // Array con todos los sonidos del salto
|
||||
std::array<JA_Sound_t*, 14> falling_sound_{}; // Array con todos los sonidos de la caída
|
||||
JumpSoundController jump_sound_ctrl_; // Controlador de sonidos de salto
|
||||
FallSoundController fall_sound_ctrl_; // Controlador de sonidos de caída
|
||||
int fall_start_position_ = 0; // Posición Y al iniciar la caída
|
||||
|
||||
void handleConveyorBelts();
|
||||
void handleShouldFall();
|
||||
void updateState(float delta_time);
|
||||
|
||||
// --- Métodos de actualización por estado ---
|
||||
void updateOnGround(float delta_time); // Actualización lógica estado ON_GROUND
|
||||
void updateOnSlope(float delta_time); // Actualización lógica estado ON_SLOPE
|
||||
void updateJumping(float delta_time); // Actualización lógica estado JUMPING
|
||||
void updateFalling(float delta_time); // Actualización lógica estado FALLING
|
||||
|
||||
// --- Métodos de movimiento por estado ---
|
||||
void moveOnGround(float delta_time); // Movimiento físico estado ON_GROUND
|
||||
void moveOnSlope(float delta_time); // Movimiento físico estado ON_SLOPE
|
||||
void moveJumping(float delta_time); // Movimiento físico estado JUMPING
|
||||
void moveFalling(float delta_time); // Movimiento físico estado FALLING
|
||||
|
||||
// --- Funciones de inicialización ---
|
||||
void initSprite(const std::string& animations_path); // Inicializa el sprite del jugador
|
||||
void initSounds(); // Inicializa los sonidos de salto y caida
|
||||
void applySpawnValues(const SpawnData& spawn); // Aplica los valores de spawn al jugador
|
||||
|
||||
// --- Funciones de procesamiento de entrada ---
|
||||
void handleInput(); // Comprueba las entradas y modifica variables
|
||||
|
||||
// --- Funciones de gestión de estado ---
|
||||
void transitionToState(State state); // Cambia el estado del jugador
|
||||
|
||||
// --- Funciones de física ---
|
||||
void applyGravity(float delta_time); // Aplica gravedad al jugador
|
||||
|
||||
// --- Funciones de movimiento y colisión ---
|
||||
void move(float delta_time); // Orquesta el movimiento del jugador
|
||||
auto getProjection(Direction direction, float displacement) -> SDL_FRect; // Devuelve el rectangulo de proyeccion
|
||||
void applyHorizontalMovement(float delta_time); // Aplica movimiento horizontal con colisión de muros
|
||||
auto handleLandingFromAir(float displacement, const SDL_FRect& projection) -> bool; // Detecta aterrizaje en superficies y rampas
|
||||
void resetSoundControllersOnLanding(); // Resetea los controladores de sonido al aterrizar
|
||||
|
||||
// --- Funciones de detección de superficies ---
|
||||
auto isOnFloor() -> bool; // Comprueba si el jugador tiene suelo debajo de los pies
|
||||
auto isOnTopSurface() -> bool; // Comprueba si el jugador está sobre una superficie
|
||||
auto isOnConveyorBelt() -> bool; // Comprueba si el jugador esta sobre una cinta transportadora
|
||||
auto isOnSlope() -> bool; // Comprueba si el jugador está sobre una rampa
|
||||
auto isLeftSlope() -> bool; // Comprueba si current_slope_ es una rampa izquierda (ascendente a la izquierda)
|
||||
void updateCurrentSlope(); // Actualiza current_slope_ con la rampa correcta y muestra debug info
|
||||
|
||||
// --- Funciones de actualización de geometría ---
|
||||
void syncSpriteAndCollider(); // Actualiza collider_box y collision points
|
||||
void updateColliderPoints(); // Actualiza los puntos de colisión
|
||||
void updateFeet(); // Actualiza los puntos de los pies
|
||||
void placeSprite(); // Coloca el sprite en la posición del jugador
|
||||
|
||||
// --- Funciones de finalización ---
|
||||
void animate(float delta_time); // Establece la animación del jugador
|
||||
auto handleBorders() -> Room::Border; // Comprueba si se halla en alguno de los cuatro bordes
|
||||
void handleJumpEnd(); // Comprueba si ha finalizado el salto al alcanzar la altura de inicio
|
||||
auto handleKillingTiles() -> bool; // Comprueba que el jugador no toque ningun tile de los que matan
|
||||
void playJumpSound(float delta_time); // Calcula y reproduce el sonido de salto
|
||||
void playFallSound(float delta_time); // Calcula y reproduce el sonido de caer
|
||||
void handleDeathByFalling(); // Gestiona la muerte al caer desde muy alto
|
||||
void updateVelocity(); // Calcula la velocidad en x
|
||||
void markAsDead(); // Marca al jugador como muerto
|
||||
};
|
||||
33
source/game/game_control.hpp
Normal file
33
source/game/game_control.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace GameControl {
|
||||
// Disponible en todos los builds — cambia la skin del jugador ("default" o nombre de enemigo)
|
||||
inline std::function<void(const std::string&)> change_player_skin;
|
||||
// Disponible en todos los builds — cambia el color del jugador (-1 = automático, 0-15 = color fijo)
|
||||
inline std::function<void(int)> change_player_color;
|
||||
} // namespace GameControl
|
||||
|
||||
#ifdef _DEBUG
|
||||
namespace GameControl {
|
||||
// Registrada por Game::Game() — cambia la habitación activa
|
||||
inline std::function<bool(const std::string&)> change_room;
|
||||
// Registrada por Game::Game() — devuelve el fichero de la habitación activa (ej. "03.yaml")
|
||||
inline std::function<std::string()> get_current_room;
|
||||
// Registrada por Game::Game() — fija el contador de items recogidos
|
||||
inline std::function<void(int)> set_items;
|
||||
// Registrada por Game::Game() — hace toggle del modo debug (equivale a tecla 0)
|
||||
inline std::function<void()> toggle_debug_mode;
|
||||
// Registrada por Game::Game() — guarda la habitación actual como habitación de inicio en debug.yaml
|
||||
inline std::function<std::string()> set_initial_room;
|
||||
// Registrada por Game::Game() — guarda la posición/flip actuales del jugador como posición de inicio en debug.yaml
|
||||
inline std::function<std::string()> set_initial_pos;
|
||||
// Registradas por Game::Game() — control del editor de mapas
|
||||
inline std::function<void()> enter_editor;
|
||||
inline std::function<void()> exit_editor;
|
||||
inline std::function<std::string()> revert_editor;
|
||||
inline std::function<void()> reload_current_room; // Recarga la habitación actual desde disco
|
||||
inline std::function<std::string(const std::string&)> get_adjacent_room; // Obtiene la room adyacente (UP/DOWN/LEFT/RIGHT)
|
||||
} // namespace GameControl
|
||||
#endif
|
||||
163
source/game/gameplay/cheevos.cpp
Normal file
163
source/game/gameplay/cheevos.cpp
Normal file
@@ -0,0 +1,163 @@
|
||||
#include "game/gameplay/cheevos.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para std::count_if
|
||||
#include <cstddef> // Para NULL
|
||||
#include <fstream> // Para basic_ostream, operator<<, basic_ofstream
|
||||
#include <iostream> // Para cout, cerr
|
||||
#include <utility>
|
||||
|
||||
#include "core/locale/locale.hpp" // Para Locale
|
||||
#include "game/options.hpp" // Para Options, options
|
||||
#include "game/ui/notifier.hpp" // Para Notifier
|
||||
|
||||
// [SINGLETON]
|
||||
Cheevos* Cheevos::cheevos = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||
void Cheevos::init(const std::string& file) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
Cheevos::cheevos = new Cheevos(file);
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
void Cheevos::destroy() {
|
||||
delete Cheevos::cheevos;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto Cheevos::get() -> Cheevos* {
|
||||
return Cheevos::cheevos;
|
||||
}
|
||||
|
||||
// Constructor
|
||||
Cheevos::Cheevos(std::string file)
|
||||
: file_(std::move(file)) {
|
||||
init();
|
||||
loadFromFile();
|
||||
}
|
||||
|
||||
// Destructor
|
||||
Cheevos::~Cheevos() {
|
||||
saveToFile();
|
||||
}
|
||||
|
||||
// Inicializa los logros
|
||||
void Cheevos::init() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
cheevos_list_.clear();
|
||||
auto* loc = Locale::get();
|
||||
cheevos_list_.emplace_back(Achievement{.id = 1, .caption = loc->get("achievements.c1"), .description = loc->get("achievements.d1"), .icon = 2});
|
||||
cheevos_list_.emplace_back(Achievement{.id = 2, .caption = loc->get("achievements.c2"), .description = loc->get("achievements.d2"), .icon = 2});
|
||||
cheevos_list_.emplace_back(Achievement{.id = 3, .caption = loc->get("achievements.c3"), .description = loc->get("achievements.d3"), .icon = 2});
|
||||
cheevos_list_.emplace_back(Achievement{.id = 4, .caption = loc->get("achievements.c4"), .description = loc->get("achievements.d4"), .icon = 2});
|
||||
cheevos_list_.emplace_back(Achievement{.id = 5, .caption = loc->get("achievements.c5"), .description = loc->get("achievements.d5"), .icon = 2});
|
||||
cheevos_list_.emplace_back(Achievement{.id = 6, .caption = loc->get("achievements.c6"), .description = loc->get("achievements.d6"), .icon = 2});
|
||||
cheevos_list_.emplace_back(Achievement{.id = 7, .caption = loc->get("achievements.c7"), .description = loc->get("achievements.d7"), .icon = 2});
|
||||
cheevos_list_.emplace_back(Achievement{.id = 8, .caption = loc->get("achievements.c8"), .description = loc->get("achievements.d8"), .icon = 2});
|
||||
cheevos_list_.emplace_back(Achievement{.id = 9, .caption = loc->get("achievements.c9"), .description = loc->get("achievements.d9"), .icon = 2});
|
||||
cheevos_list_.emplace_back(Achievement{.id = 10, .caption = loc->get("achievements.c10"), .description = loc->get("achievements.d10"), .icon = 2});
|
||||
cheevos_list_.emplace_back(Achievement{.id = 11, .caption = loc->get("achievements.c11"), .description = loc->get("achievements.d11"), .icon = 2});
|
||||
cheevos_list_.emplace_back(Achievement{.id = 12, .caption = loc->get("achievements.c12"), .description = loc->get("achievements.d12"), .icon = 2});
|
||||
}
|
||||
|
||||
// Busca un logro por id y devuelve el indice
|
||||
auto Cheevos::find(int id) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
for (int i = 0; i < (int)cheevos_list_.size(); ++i) {
|
||||
if (cheevos_list_[i].id == id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Desbloquea un logro
|
||||
void Cheevos::unlock(int id) {
|
||||
const int INDEX = find(id);
|
||||
|
||||
// Si el índice es inválido, el logro no es válido, ya está completado o el sistema de logros no está habilitado, no hacemos nada
|
||||
if (INDEX == -1 || !cheevos_list_.at(INDEX).obtainable || cheevos_list_.at(INDEX).completed || !enabled_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Marcar el logro como completado
|
||||
cheevos_list_.at(INDEX).completed = true;
|
||||
|
||||
// Mostrar notificación en la pantalla
|
||||
Notifier::get()->show({Locale::get()->get("achievements.header"), cheevos_list_.at(INDEX).caption}, Notifier::Style::CHEEVO, -1, false);
|
||||
|
||||
// Guardar el estado de los logros
|
||||
saveToFile();
|
||||
}
|
||||
|
||||
// Invalida un logro
|
||||
void Cheevos::setUnobtainable(int id) {
|
||||
const int INDEX = find(id);
|
||||
|
||||
// Si el índice es válido, se invalida el logro
|
||||
if (INDEX != -1) {
|
||||
cheevos_list_.at(INDEX).obtainable = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Carga el estado de los logros desde un fichero
|
||||
void Cheevos::loadFromFile() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
std::ifstream file(file_, std::ios::binary);
|
||||
|
||||
// El fichero no existe
|
||||
if (!file) {
|
||||
std::cout << "Warning: Unable to open " << file_ << "! Creating new file..." << '\n';
|
||||
|
||||
// Crea el fichero en modo escritura (binario)
|
||||
std::ofstream new_file(file_, std::ios::binary);
|
||||
|
||||
if (new_file) {
|
||||
std::cout << "New " << file_ << " created!" << '\n';
|
||||
|
||||
// Guarda la información
|
||||
for (const auto& cheevo : cheevos_list_) {
|
||||
new_file.write(reinterpret_cast<const char*>(&cheevo.completed), sizeof(bool));
|
||||
}
|
||||
} else {
|
||||
std::cerr << "Error: Unable to create " << file_ << "!" << '\n';
|
||||
}
|
||||
}
|
||||
// El fichero existe
|
||||
else {
|
||||
std::cout << "Reading " << file_ << '\n';
|
||||
|
||||
// Carga los datos
|
||||
for (auto& cheevo : cheevos_list_) {
|
||||
file.read(reinterpret_cast<char*>(&cheevo.completed), sizeof(bool));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Guarda el estado de los logros en un fichero
|
||||
void Cheevos::saveToFile() {
|
||||
// Abre el fichero en modo escritura (binario)
|
||||
SDL_IOStream* file = SDL_IOFromFile(this->file_.c_str(), "w+b");
|
||||
if (file != nullptr) {
|
||||
// Guarda la información
|
||||
for (auto& i : cheevos_list_) {
|
||||
SDL_WriteIO(file, &i.completed, sizeof(bool));
|
||||
}
|
||||
|
||||
// Cierra el fichero
|
||||
SDL_CloseIO(file);
|
||||
} else {
|
||||
std::cout << "Error: Unable to save file! " << SDL_GetError() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Devuelve el número total de logros desbloqueados
|
||||
auto Cheevos::getTotalUnlockedAchievements() -> int {
|
||||
return std::count_if(cheevos_list_.begin(), cheevos_list_.end(), [](const auto& cheevo) -> bool { return cheevo.completed; });
|
||||
}
|
||||
|
||||
// Elimina el estado "no obtenible"
|
||||
void Cheevos::clearUnobtainableState() {
|
||||
for (auto& cheevo : cheevos_list_) {
|
||||
cheevo.obtainable = true;
|
||||
}
|
||||
}
|
||||
55
source/game/gameplay/cheevos.hpp
Normal file
55
source/game/gameplay/cheevos.hpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <string> // Para string
|
||||
#include <utility>
|
||||
#include <vector> // Para vector
|
||||
|
||||
class Cheevos {
|
||||
public:
|
||||
// Tipos anidados (públicos porque se usan en la interfaz)
|
||||
struct Achievement {
|
||||
int id{0}; // Identificador del logro
|
||||
std::string caption; // Texto con el nombre del logro
|
||||
std::string description; // Texto que describe el logro
|
||||
int icon{0}; // Indice del icono a utilizar en la notificación
|
||||
bool completed{false}; // Indica si se ha obtenido el logro
|
||||
bool obtainable{true}; // Indica si se puede obtener el logro
|
||||
};
|
||||
|
||||
using Achievements = std::vector<Achievement>; // Type alias para vector de logros
|
||||
|
||||
// Gestión singleton
|
||||
static void init(const std::string& file); // Inicialización
|
||||
static void destroy(); // Destrucción
|
||||
static auto get() -> Cheevos*; // Acceso al singleton
|
||||
|
||||
// Gestión de logros
|
||||
void unlock(int id); // Desbloquea un logro
|
||||
void setUnobtainable(int id); // Invalida un logro
|
||||
void clearUnobtainableState(); // Elimina el estado "no obtenible"
|
||||
void enable(bool value) { enabled_ = value; } // Habilita o deshabilita los logros
|
||||
|
||||
// Consultas
|
||||
[[nodiscard]] auto list() const -> const Achievements& { return cheevos_list_; } // Lista los logros
|
||||
auto getTotalUnlockedAchievements() -> int; // Devuelve logros desbloqueados
|
||||
auto size() -> int { return cheevos_list_.size(); } // Devuelve número total de logros
|
||||
|
||||
private:
|
||||
// Constantes singleton
|
||||
static Cheevos* cheevos; // [SINGLETON] Objeto privado
|
||||
|
||||
// Métodos privados
|
||||
void init(); // Inicializa los logros
|
||||
auto find(int id) -> int; // Busca un logro por id y devuelve el índice
|
||||
void loadFromFile(); // Carga el estado de los logros desde un fichero
|
||||
void saveToFile(); // Guarda el estado de los logros en un fichero
|
||||
|
||||
// Constructor y destructor privados [SINGLETON]
|
||||
explicit Cheevos(std::string file);
|
||||
~Cheevos();
|
||||
|
||||
// Variables miembro
|
||||
Achievements cheevos_list_; // Listado de logros
|
||||
bool enabled_{true}; // Indica si los logros se pueden obtener
|
||||
std::string file_; // Fichero donde leer/almacenar el estado de los logros
|
||||
};
|
||||
510
source/game/gameplay/collision_map.cpp
Normal file
510
source/game/gameplay/collision_map.cpp
Normal file
@@ -0,0 +1,510 @@
|
||||
#include "collision_map.hpp"
|
||||
|
||||
#include <algorithm> // Para std::ranges::any_of
|
||||
|
||||
#ifdef _DEBUG
|
||||
#include "core/system/debug.hpp" // Para Debug
|
||||
#endif
|
||||
#include "utils/defines.hpp" // Para Collision
|
||||
|
||||
// Constructor
|
||||
CollisionMap::CollisionMap(std::vector<int> tile_map, int tile_set_width, int conveyor_belt_direction)
|
||||
: tile_map_(std::move(tile_map)),
|
||||
tile_set_width_(tile_set_width),
|
||||
conveyor_belt_direction_(conveyor_belt_direction) {
|
||||
// Inicializa todas las superficies de colisión
|
||||
initializeSurfaces();
|
||||
}
|
||||
|
||||
// Inicializa todas las superficies de colisión
|
||||
void CollisionMap::initializeSurfaces() {
|
||||
setBottomSurfaces();
|
||||
setTopSurfaces();
|
||||
setLeftSurfaces();
|
||||
setRightSurfaces();
|
||||
setLeftSlopes();
|
||||
setRightSlopes();
|
||||
setAutoSurfaces();
|
||||
}
|
||||
|
||||
// Devuelve el tipo de tile que hay en ese pixel
|
||||
auto CollisionMap::getTile(SDL_FPoint point) const -> Tile {
|
||||
const int ROW = static_cast<int>(point.y / TILE_SIZE);
|
||||
const int COL = static_cast<int>(point.x / TILE_SIZE);
|
||||
const int POS = (ROW * MAP_WIDTH) + COL;
|
||||
return getTile(POS);
|
||||
}
|
||||
|
||||
// Devuelve el tipo de tile que hay en ese indice
|
||||
auto CollisionMap::getTile(int index) const -> Tile { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const bool ON_RANGE = (index > -1) && (index < (int)tile_map_.size());
|
||||
|
||||
if (ON_RANGE) {
|
||||
// Las filas 0-8 son de tiles t_wall
|
||||
if ((tile_map_[index] >= 0) && (tile_map_[index] < 9 * tile_set_width_)) {
|
||||
return Tile::WALL;
|
||||
}
|
||||
|
||||
// Las filas 9-17 son de tiles t_passable
|
||||
if ((tile_map_[index] >= 9 * tile_set_width_) && (tile_map_[index] < 18 * tile_set_width_)) {
|
||||
return Tile::PASSABLE;
|
||||
}
|
||||
|
||||
// Las filas 18-20 es de tiles t_animated
|
||||
if ((tile_map_[index] >= 18 * tile_set_width_) && (tile_map_[index] < 21 * tile_set_width_)) {
|
||||
return Tile::ANIMATED;
|
||||
}
|
||||
|
||||
// La fila 21 es de tiles t_slope_r
|
||||
if ((tile_map_[index] >= 21 * tile_set_width_) && (tile_map_[index] < 22 * tile_set_width_)) {
|
||||
return Tile::SLOPE_R;
|
||||
}
|
||||
|
||||
// La fila 22 es de tiles t_slope_l
|
||||
if ((tile_map_[index] >= 22 * tile_set_width_) && (tile_map_[index] < 23 * tile_set_width_)) {
|
||||
return Tile::SLOPE_L;
|
||||
}
|
||||
|
||||
// La fila 23 es de tiles t_kill
|
||||
if ((tile_map_[index] >= 23 * tile_set_width_) && (tile_map_[index] < 24 * tile_set_width_)) {
|
||||
return Tile::KILL;
|
||||
}
|
||||
}
|
||||
|
||||
return Tile::EMPTY;
|
||||
}
|
||||
|
||||
// Obten la coordenada de la cuesta a partir de un punto perteneciente a ese tile
|
||||
auto CollisionMap::getSlopeHeight(SDL_FPoint p, Tile slope) -> int {
|
||||
// Calcula la base del tile
|
||||
int base = ((p.y / TILE_SIZE) * TILE_SIZE) + TILE_SIZE;
|
||||
#ifdef _DEBUG
|
||||
Debug::get()->set("slope.BASE", std::to_string(base));
|
||||
#endif
|
||||
|
||||
// Calcula cuanto se ha entrado en el tile horizontalmente
|
||||
const int POS = (static_cast<int>(p.x) % TILE_SIZE); // Esto da un valor entre 0 y 7
|
||||
#ifdef _DEBUG
|
||||
Debug::get()->set("slope.POS", std::to_string(POS));
|
||||
#endif
|
||||
|
||||
// Se resta a la base la cantidad de pixeles pos en funcion de la rampa
|
||||
if (slope == Tile::SLOPE_R) {
|
||||
base -= POS + 1;
|
||||
#ifdef _DEBUG
|
||||
Debug::get()->set("slope.result", "BASE_R=" + std::to_string(base));
|
||||
#endif
|
||||
} else {
|
||||
base -= (TILE_SIZE - POS);
|
||||
#ifdef _DEBUG
|
||||
Debug::get()->set("slope.result", "BASE_L=" + std::to_string(base));
|
||||
#endif
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
// === Queries de colisión ===
|
||||
|
||||
// Comprueba las colisiones con paredes derechas
|
||||
auto CollisionMap::checkRightSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
for (const auto& s : right_walls_) {
|
||||
if (checkCollision(s, rect)) {
|
||||
return s.x;
|
||||
}
|
||||
}
|
||||
return Collision::NONE;
|
||||
}
|
||||
|
||||
// Comprueba las colisiones con paredes izquierdas
|
||||
auto CollisionMap::checkLeftSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
for (const auto& s : left_walls_) {
|
||||
if (checkCollision(s, rect)) {
|
||||
return s.x;
|
||||
}
|
||||
}
|
||||
return Collision::NONE;
|
||||
}
|
||||
|
||||
// Comprueba las colisiones con techos
|
||||
auto CollisionMap::checkTopSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
for (const auto& s : top_floors_) {
|
||||
if (checkCollision(s, rect)) {
|
||||
return s.y;
|
||||
}
|
||||
}
|
||||
return Collision::NONE;
|
||||
}
|
||||
|
||||
// Comprueba las colisiones punto con techos
|
||||
auto CollisionMap::checkTopSurfaces(const SDL_FPoint& p) -> bool {
|
||||
return std::ranges::any_of(top_floors_, [&](const auto& s) -> bool {
|
||||
return checkCollision(s, p);
|
||||
});
|
||||
}
|
||||
|
||||
// Comprueba las colisiones con suelos
|
||||
auto CollisionMap::checkBottomSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
for (const auto& s : bottom_floors_) {
|
||||
if (checkCollision(s, rect)) {
|
||||
return s.y;
|
||||
}
|
||||
}
|
||||
return Collision::NONE;
|
||||
}
|
||||
|
||||
// Comprueba las colisiones con conveyor belts
|
||||
auto CollisionMap::checkAutoSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
for (const auto& s : conveyor_belt_floors_) {
|
||||
if (checkCollision(s, rect)) {
|
||||
return s.y;
|
||||
}
|
||||
}
|
||||
return Collision::NONE;
|
||||
}
|
||||
|
||||
// Comprueba las colisiones punto con conveyor belts
|
||||
auto CollisionMap::checkConveyorBelts(const SDL_FPoint& p) -> bool {
|
||||
return std::ranges::any_of(conveyor_belt_floors_, [&](const auto& s) -> bool {
|
||||
return checkCollision(s, p);
|
||||
});
|
||||
}
|
||||
|
||||
// Comprueba las colisiones línea con rampas izquierdas
|
||||
auto CollisionMap::checkLeftSlopes(const LineVertical& line) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
for (const auto& slope : left_slopes_) {
|
||||
const auto P = checkCollision(slope, line);
|
||||
if (P.x != -1) {
|
||||
return P.y;
|
||||
}
|
||||
}
|
||||
return Collision::NONE;
|
||||
}
|
||||
|
||||
// Comprueba las colisiones punto con rampas izquierdas
|
||||
auto CollisionMap::checkLeftSlopes(const SDL_FPoint& p) -> bool {
|
||||
return std::ranges::any_of(left_slopes_, [&](const auto& slope) -> bool {
|
||||
return checkCollision(p, slope);
|
||||
});
|
||||
}
|
||||
|
||||
// Comprueba las colisiones línea con rampas derechas
|
||||
auto CollisionMap::checkRightSlopes(const LineVertical& line) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
for (const auto& slope : right_slopes_) {
|
||||
const auto P = checkCollision(slope, line);
|
||||
if (P.x != -1) {
|
||||
return P.y;
|
||||
}
|
||||
}
|
||||
return Collision::NONE;
|
||||
}
|
||||
|
||||
// Comprueba las colisiones punto con rampas derechas
|
||||
auto CollisionMap::checkRightSlopes(const SDL_FPoint& p) -> bool {
|
||||
return std::ranges::any_of(right_slopes_, [&](const auto& slope) -> bool {
|
||||
return checkCollision(p, slope);
|
||||
});
|
||||
}
|
||||
|
||||
// Obtiene puntero a slope en un punto (prioriza left_slopes_ sobre right_slopes_)
|
||||
auto CollisionMap::getSlopeAtPoint(const SDL_FPoint& p) const -> const LineDiagonal* { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Primero busca en rampas izquierdas
|
||||
for (const auto& slope : left_slopes_) {
|
||||
if (checkCollision(p, slope)) {
|
||||
return &slope;
|
||||
}
|
||||
}
|
||||
|
||||
// Luego busca en rampas derechas
|
||||
for (const auto& slope : right_slopes_) {
|
||||
if (checkCollision(p, slope)) {
|
||||
return &slope;
|
||||
}
|
||||
}
|
||||
|
||||
// No hay colisión con ninguna slope
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// === Helpers para recopilar tiles ===
|
||||
|
||||
// Helper: recopila tiles inferiores (muros sin muro debajo)
|
||||
auto CollisionMap::collectBottomTiles() -> std::vector<int> { // NOLINT(readability-make-member-function-const)
|
||||
std::vector<int> tile;
|
||||
|
||||
// Busca todos los tiles de tipo muro que no tengan debajo otro muro
|
||||
// Hay que recorrer la habitación por filas (excepto los de la última fila)
|
||||
for (int i = 0; i < (int)tile_map_.size() - MAP_WIDTH; ++i) {
|
||||
if (getTile(i) == Tile::WALL && getTile(i + MAP_WIDTH) != Tile::WALL) {
|
||||
tile.push_back(i);
|
||||
|
||||
// Si llega al final de la fila, introduce un separador
|
||||
if (i % MAP_WIDTH == MAP_WIDTH - 1) {
|
||||
tile.push_back(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Añade un terminador
|
||||
tile.push_back(-1);
|
||||
return tile;
|
||||
}
|
||||
|
||||
// Helper: recopila tiles superiores (muros o pasables sin muro encima)
|
||||
auto CollisionMap::collectTopTiles() -> std::vector<int> { // NOLINT(readability-make-member-function-const)
|
||||
std::vector<int> tile;
|
||||
|
||||
// Busca todos los tiles de tipo muro o pasable que no tengan encima un muro
|
||||
// Hay que recorrer la habitación por filas (excepto los de la primera fila)
|
||||
for (int i = MAP_WIDTH; i < (int)tile_map_.size(); ++i) {
|
||||
if ((getTile(i) == Tile::WALL || getTile(i) == Tile::PASSABLE) && getTile(i - MAP_WIDTH) != Tile::WALL) {
|
||||
tile.push_back(i);
|
||||
|
||||
// Si llega al final de la fila, introduce un separador
|
||||
if (i % MAP_WIDTH == MAP_WIDTH - 1) {
|
||||
tile.push_back(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Añade un terminador
|
||||
tile.push_back(-1);
|
||||
return tile;
|
||||
}
|
||||
|
||||
// Helper: recopila tiles animados (para superficies automaticas/conveyor belts)
|
||||
auto CollisionMap::collectAnimatedTiles() -> std::vector<int> { // NOLINT(readability-make-member-function-const)
|
||||
std::vector<int> tile;
|
||||
|
||||
// Busca todos los tiles de tipo animado
|
||||
// Hay que recorrer la habitación por filas (excepto los de la primera fila)
|
||||
for (int i = MAP_WIDTH; i < (int)tile_map_.size(); ++i) {
|
||||
if (getTile(i) == Tile::ANIMATED) {
|
||||
tile.push_back(i);
|
||||
|
||||
// Si llega al final de la fila, introduce un separador
|
||||
if (i % MAP_WIDTH == MAP_WIDTH - 1) {
|
||||
tile.push_back(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Añade un terminador si hay tiles
|
||||
if (!tile.empty()) {
|
||||
tile.push_back(-1);
|
||||
}
|
||||
|
||||
return tile;
|
||||
}
|
||||
|
||||
// Helper: construye lineas horizontales a partir de tiles consecutivos
|
||||
void CollisionMap::buildHorizontalLines(const std::vector<int>& tiles, std::vector<LineHorizontal>& lines, bool is_bottom_surface) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (tiles.size() <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
while (i < static_cast<int>(tiles.size()) - 1) {
|
||||
LineHorizontal line;
|
||||
line.x1 = (tiles[i] % MAP_WIDTH) * TILE_SIZE;
|
||||
|
||||
// Calcula Y segun si es superficie inferior o superior
|
||||
if (is_bottom_surface) {
|
||||
line.y = ((tiles[i] / MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
|
||||
} else {
|
||||
line.y = (tiles[i] / MAP_WIDTH) * TILE_SIZE;
|
||||
}
|
||||
|
||||
int last_one = i;
|
||||
i++;
|
||||
|
||||
// Encuentra tiles consecutivos
|
||||
if (i < static_cast<int>(tiles.size())) {
|
||||
while (tiles[i] == tiles[i - 1] + 1) {
|
||||
last_one = i;
|
||||
i++;
|
||||
if (i >= static_cast<int>(tiles.size())) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
line.x2 = ((tiles[last_one] % MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
|
||||
lines.push_back(line);
|
||||
|
||||
// Salta separadores
|
||||
if (i < static_cast<int>(tiles.size()) && tiles[i] == -1) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Métodos de generación de geometría ===
|
||||
|
||||
// Calcula las superficies inferiores
|
||||
void CollisionMap::setBottomSurfaces() {
|
||||
std::vector<int> tile = collectBottomTiles();
|
||||
buildHorizontalLines(tile, bottom_floors_, true);
|
||||
}
|
||||
|
||||
// Calcula las superficies superiores
|
||||
void CollisionMap::setTopSurfaces() {
|
||||
std::vector<int> tile = collectTopTiles();
|
||||
buildHorizontalLines(tile, top_floors_, false);
|
||||
}
|
||||
|
||||
// Calcula las superficies laterales izquierdas
|
||||
void CollisionMap::setLeftSurfaces() { // NOLINT(readability-make-member-function-const)
|
||||
std::vector<int> tile;
|
||||
|
||||
// Busca todos los tiles de tipo muro que no tienen a su izquierda un tile de tipo muro
|
||||
// Hay que recorrer la habitación por columnas (excepto los de la primera columna)
|
||||
for (int i = 1; i < MAP_WIDTH; ++i) {
|
||||
for (int j = 0; j < MAP_HEIGHT; ++j) {
|
||||
const int POS = ((j * MAP_WIDTH) + i);
|
||||
if (getTile(POS) == Tile::WALL && getTile(POS - 1) != Tile::WALL) {
|
||||
tile.push_back(POS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Añade un terminador
|
||||
tile.push_back(-1);
|
||||
|
||||
// Recorre el vector de tiles buscando tiles consecutivos
|
||||
// (Los tiles de la misma columna, la diferencia entre ellos es de mapWidth)
|
||||
// para localizar las superficies
|
||||
if ((int)tile.size() > 1) {
|
||||
int i = 0;
|
||||
do {
|
||||
LineVertical line;
|
||||
line.x = (tile[i] % MAP_WIDTH) * TILE_SIZE;
|
||||
line.y1 = ((tile[i] / MAP_WIDTH) * TILE_SIZE);
|
||||
while (tile[i] + MAP_WIDTH == tile[i + 1]) {
|
||||
if (i == (int)tile.size() - 1) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
line.y2 = ((tile[i] / MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
|
||||
left_walls_.push_back(line);
|
||||
i++;
|
||||
} while (i < (int)tile.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Calcula las superficies laterales derechas
|
||||
void CollisionMap::setRightSurfaces() { // NOLINT(readability-make-member-function-const)
|
||||
std::vector<int> tile;
|
||||
|
||||
// Busca todos los tiles de tipo muro que no tienen a su derecha un tile de tipo muro
|
||||
// Hay que recorrer la habitación por columnas (excepto los de la última columna)
|
||||
for (int i = 0; i < MAP_WIDTH - 1; ++i) {
|
||||
for (int j = 0; j < MAP_HEIGHT; ++j) {
|
||||
const int POS = ((j * MAP_WIDTH) + i);
|
||||
if (getTile(POS) == Tile::WALL && getTile(POS + 1) != Tile::WALL) {
|
||||
tile.push_back(POS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Añade un terminador
|
||||
tile.push_back(-1);
|
||||
|
||||
// Recorre el vector de tiles buscando tiles consecutivos
|
||||
// (Los tiles de la misma columna, la diferencia entre ellos es de mapWidth)
|
||||
// para localizar las superficies
|
||||
if ((int)tile.size() > 1) {
|
||||
int i = 0;
|
||||
do {
|
||||
LineVertical line;
|
||||
line.x = ((tile[i] % MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
|
||||
line.y1 = ((tile[i] / MAP_WIDTH) * TILE_SIZE);
|
||||
while (tile[i] + MAP_WIDTH == tile[i + 1]) {
|
||||
if (i == (int)tile.size() - 1) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
line.y2 = ((tile[i] / MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
|
||||
right_walls_.push_back(line);
|
||||
i++;
|
||||
} while (i < (int)tile.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Encuentra todas las rampas que suben hacia la izquierda
|
||||
void CollisionMap::setLeftSlopes() { // NOLINT(readability-make-member-function-const)
|
||||
// Recorre la habitación entera por filas buscando tiles de tipo t_slope_l
|
||||
std::vector<int> found;
|
||||
for (int i = 0; i < (int)tile_map_.size(); ++i) {
|
||||
if (getTile(i) == Tile::SLOPE_L) {
|
||||
found.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
// El primer elemento es el inicio de una rampa. Se añade ese elemento y se buscan los siguientes,
|
||||
// que seran i + mapWidth + 1. Conforme se añaden se eliminan y se vuelve a escudriñar el vector de
|
||||
// tiles encontrados hasta que esté vacío
|
||||
|
||||
while (!found.empty()) {
|
||||
LineDiagonal line;
|
||||
line.x1 = (found[0] % MAP_WIDTH) * TILE_SIZE;
|
||||
line.y1 = (found[0] / MAP_WIDTH) * TILE_SIZE;
|
||||
int looking_for = found[0] + MAP_WIDTH + 1;
|
||||
int last_one_found = found[0];
|
||||
found.erase(found.begin());
|
||||
for (int i = 0; i < (int)found.size(); ++i) {
|
||||
if (found[i] == looking_for) {
|
||||
last_one_found = looking_for;
|
||||
looking_for += MAP_WIDTH + 1;
|
||||
found.erase(found.begin() + i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
line.x2 = ((last_one_found % MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
|
||||
line.y2 = ((last_one_found / MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
|
||||
left_slopes_.push_back(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Encuentra todas las rampas que suben hacia la derecha
|
||||
void CollisionMap::setRightSlopes() { // NOLINT(readability-make-member-function-const)
|
||||
// Recorre la habitación entera por filas buscando tiles de tipo t_slope_r
|
||||
std::vector<int> found;
|
||||
for (int i = 0; i < (int)tile_map_.size(); ++i) {
|
||||
if (getTile(i) == Tile::SLOPE_R) {
|
||||
found.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
// El primer elemento es el inicio de una rampa. Se añade ese elemento y se buscan los siguientes,
|
||||
// que seran i + mapWidth - 1. Conforme se añaden se eliminan y se vuelve a escudriñar el vector de
|
||||
// tiles encontrados hasta que esté vacío
|
||||
|
||||
while (!found.empty()) {
|
||||
LineDiagonal line;
|
||||
line.x1 = ((found[0] % MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
|
||||
line.y1 = (found[0] / MAP_WIDTH) * TILE_SIZE;
|
||||
int looking_for = found[0] + MAP_WIDTH - 1;
|
||||
int last_one_found = found[0];
|
||||
found.erase(found.begin());
|
||||
for (int i = 0; i < (int)found.size(); ++i) {
|
||||
if (found[i] == looking_for) {
|
||||
last_one_found = looking_for;
|
||||
looking_for += MAP_WIDTH - 1;
|
||||
found.erase(found.begin() + i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
line.x2 = (last_one_found % MAP_WIDTH) * TILE_SIZE;
|
||||
line.y2 = ((last_one_found / MAP_WIDTH) * TILE_SIZE) + TILE_SIZE - 1;
|
||||
right_slopes_.push_back(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Calcula las superficies automaticas (conveyor belts)
|
||||
void CollisionMap::setAutoSurfaces() {
|
||||
std::vector<int> tile = collectAnimatedTiles();
|
||||
buildHorizontalLines(tile, conveyor_belt_floors_, false);
|
||||
}
|
||||
121
source/game/gameplay/collision_map.hpp
Normal file
121
source/game/gameplay/collision_map.hpp
Normal file
@@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "utils/utils.hpp" // Para LineHorizontal, LineDiagonal, LineVertical
|
||||
|
||||
/**
|
||||
* @brief Mapa de colisiones de una habitación
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Almacenar la geometría de colisión (superficies, rampas, conveyor belts)
|
||||
* - Generar geometría a partir del tilemap
|
||||
* - Proporcionar queries de colisión para Player y otras entidades
|
||||
* - Determinar tipo de tile en posiciones específicas
|
||||
*/
|
||||
class CollisionMap {
|
||||
public:
|
||||
// Enumeración de tipos de tile (para colisiones)
|
||||
enum class Tile {
|
||||
EMPTY,
|
||||
WALL,
|
||||
PASSABLE,
|
||||
SLOPE_L,
|
||||
SLOPE_R,
|
||||
KILL,
|
||||
ANIMATED
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Constructor
|
||||
* @param tile_map Vector con índices de tiles de la habitación
|
||||
* @param tile_set_width Ancho del tileset en tiles (para calcular tipo de tile)
|
||||
* @param conveyor_belt_direction Dirección de las cintas transportadoras (-1, 0, +1)
|
||||
*/
|
||||
CollisionMap(std::vector<int> tile_map, int tile_set_width, int conveyor_belt_direction);
|
||||
~CollisionMap() = default;
|
||||
|
||||
// Prohibir copia y movimiento
|
||||
CollisionMap(const CollisionMap&) = delete;
|
||||
auto operator=(const CollisionMap&) -> CollisionMap& = delete;
|
||||
CollisionMap(CollisionMap&&) = delete;
|
||||
auto operator=(CollisionMap&&) -> CollisionMap& = delete;
|
||||
|
||||
// --- Queries de tipo de tile ---
|
||||
[[nodiscard]] auto getTile(SDL_FPoint point) const -> Tile; // Devuelve el tipo de tile en un punto (pixel)
|
||||
[[nodiscard]] auto getTile(int index) const -> Tile; // Devuelve el tipo de tile en un índice del tilemap
|
||||
|
||||
// --- Queries de colisión con superficies ---
|
||||
auto checkRightSurfaces(const SDL_FRect& rect) -> int; // Colisión con paredes derechas (retorna X)
|
||||
auto checkLeftSurfaces(const SDL_FRect& rect) -> int; // Colisión con paredes izquierdas (retorna X)
|
||||
auto checkTopSurfaces(const SDL_FRect& rect) -> int; // Colisión con techos (retorna Y)
|
||||
auto checkTopSurfaces(const SDL_FPoint& p) -> bool; // Colisión punto con techos
|
||||
auto checkBottomSurfaces(const SDL_FRect& rect) -> int; // Colisión con suelos (retorna Y)
|
||||
|
||||
// --- Queries de colisión con superficies automáticas (conveyor belts) ---
|
||||
auto checkAutoSurfaces(const SDL_FRect& rect) -> int; // Colisión con conveyor belts (retorna Y)
|
||||
auto checkConveyorBelts(const SDL_FPoint& p) -> bool; // Colisión punto con conveyor belts
|
||||
|
||||
// --- Queries de colisión con rampas ---
|
||||
auto checkLeftSlopes(const LineVertical& line) -> int; // Colisión línea con rampas izquierdas (retorna Y)
|
||||
auto checkLeftSlopes(const SDL_FPoint& p) -> bool; // Colisión punto con rampas izquierdas
|
||||
auto checkRightSlopes(const LineVertical& line) -> int; // Colisión línea con rampas derechas (retorna Y)
|
||||
auto checkRightSlopes(const SDL_FPoint& p) -> bool; // Colisión punto con rampas derechas
|
||||
[[nodiscard]] auto getSlopeAtPoint(const SDL_FPoint& p) const -> const LineDiagonal*; // Obtiene puntero a slope en un punto
|
||||
|
||||
// --- Métodos estáticos ---
|
||||
static auto getTileSize() -> int { return TILE_SIZE; } // Tamaño del tile en pixels
|
||||
static auto getSlopeHeight(SDL_FPoint p, Tile slope) -> int; // Altura de rampa en un punto
|
||||
|
||||
// --- Getters ---
|
||||
[[nodiscard]] auto getConveyorBeltDirection() const -> int { return conveyor_belt_direction_; }
|
||||
|
||||
// Getters para debug visualization
|
||||
[[nodiscard]] auto getBottomFloors() const -> const std::vector<LineHorizontal>& { return bottom_floors_; }
|
||||
[[nodiscard]] auto getTopFloors() const -> const std::vector<LineHorizontal>& { return top_floors_; }
|
||||
[[nodiscard]] auto getLeftWalls() const -> const std::vector<LineVertical>& { return left_walls_; }
|
||||
[[nodiscard]] auto getRightWalls() const -> const std::vector<LineVertical>& { return right_walls_; }
|
||||
[[nodiscard]] auto getLeftSlopes() const -> const std::vector<LineDiagonal>& { return left_slopes_; }
|
||||
[[nodiscard]] auto getRightSlopes() const -> const std::vector<LineDiagonal>& { return right_slopes_; }
|
||||
[[nodiscard]] auto getConveyorBeltFloors() const -> const std::vector<LineHorizontal>& { return conveyor_belt_floors_; }
|
||||
|
||||
private:
|
||||
// --- Constantes ---
|
||||
static constexpr int TILE_SIZE = 8; // Tamaño del tile en pixels
|
||||
static constexpr int MAP_WIDTH = 32; // Ancho del mapa en tiles
|
||||
static constexpr int MAP_HEIGHT = 16; // Alto del mapa en tiles
|
||||
|
||||
// --- Datos de la habitación ---
|
||||
std::vector<int> tile_map_; // Índices de tiles de la habitación
|
||||
int tile_set_width_; // Ancho del tileset en tiles
|
||||
int conveyor_belt_direction_; // Dirección de conveyor belts
|
||||
|
||||
// --- Geometría de colisión ---
|
||||
std::vector<LineHorizontal> bottom_floors_; // Superficies inferiores (suelos)
|
||||
std::vector<LineHorizontal> top_floors_; // Superficies superiores (techos)
|
||||
std::vector<LineVertical> left_walls_; // Paredes izquierdas
|
||||
std::vector<LineVertical> right_walls_; // Paredes derechas
|
||||
std::vector<LineDiagonal> left_slopes_; // Rampas que suben hacia la izquierda
|
||||
std::vector<LineDiagonal> right_slopes_; // Rampas que suben hacia la derecha
|
||||
std::vector<LineHorizontal> conveyor_belt_floors_; // Superficies automáticas (conveyor belts)
|
||||
|
||||
// --- Métodos privados de generación de geometría ---
|
||||
void initializeSurfaces(); // Inicializa todas las superficies de colisión
|
||||
|
||||
// Helpers para recopilar tiles
|
||||
auto collectBottomTiles() -> std::vector<int>; // Tiles con superficie inferior
|
||||
auto collectTopTiles() -> std::vector<int>; // Tiles con superficie superior
|
||||
auto collectAnimatedTiles() -> std::vector<int>; // Tiles animados (conveyor belts)
|
||||
|
||||
// Construcción de geometría
|
||||
static void buildHorizontalLines(const std::vector<int>& tiles, std::vector<LineHorizontal>& lines, bool is_bottom_surface);
|
||||
void setBottomSurfaces(); // Calcula superficies inferiores
|
||||
void setTopSurfaces(); // Calcula superficies superiores
|
||||
void setLeftSurfaces(); // Calcula paredes izquierdas
|
||||
void setRightSurfaces(); // Calcula paredes derechas
|
||||
void setLeftSlopes(); // Calcula rampas izquierdas
|
||||
void setRightSlopes(); // Calcula rampas derechas
|
||||
void setAutoSurfaces(); // Calcula conveyor belts
|
||||
};
|
||||
76
source/game/gameplay/enemy_manager.cpp
Normal file
76
source/game/gameplay/enemy_manager.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "enemy_manager.hpp"
|
||||
|
||||
#include <algorithm> // Para std::ranges::any_of
|
||||
|
||||
#include "game/entities/enemy.hpp" // Para Enemy
|
||||
#include "utils/utils.hpp" // Para checkCollision
|
||||
|
||||
// Añade un enemigo a la colección
|
||||
void EnemyManager::addEnemy(std::shared_ptr<Enemy> enemy) { // NOLINT(readability-identifier-naming)
|
||||
enemies_.push_back(std::move(enemy));
|
||||
}
|
||||
|
||||
// Elimina todos los enemigos
|
||||
void EnemyManager::clear() {
|
||||
enemies_.clear();
|
||||
}
|
||||
|
||||
// Elimina el último enemigo de la colección
|
||||
void EnemyManager::removeLastEnemy() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (!enemies_.empty()) {
|
||||
enemies_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
// Comprueba si no hay enemigos
|
||||
auto EnemyManager::isEmpty() const -> bool {
|
||||
return enemies_.empty();
|
||||
}
|
||||
|
||||
// Actualiza todos los enemigos
|
||||
void EnemyManager::update(float delta_time) {
|
||||
for (const auto& enemy : enemies_) {
|
||||
enemy->update(delta_time);
|
||||
}
|
||||
}
|
||||
|
||||
// Renderiza todos los enemigos
|
||||
void EnemyManager::render() {
|
||||
for (const auto& enemy : enemies_) {
|
||||
enemy->render();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Solo actualiza animaciones sin mover enemigos
|
||||
void EnemyManager::updateAnimations(float delta_time) {
|
||||
for (const auto& enemy : enemies_) {
|
||||
enemy->updateAnimation(delta_time);
|
||||
}
|
||||
}
|
||||
|
||||
// Resetea todos los enemigos a su posición inicial
|
||||
void EnemyManager::resetPositions(const std::vector<Enemy::Data>& enemy_data) {
|
||||
const int COUNT = std::min(static_cast<int>(enemies_.size()), static_cast<int>(enemy_data.size()));
|
||||
for (int i = 0; i < COUNT; ++i) {
|
||||
enemies_[i]->resetToInitialPosition(enemy_data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Número de enemigos
|
||||
auto EnemyManager::getCount() const -> int {
|
||||
return static_cast<int>(enemies_.size());
|
||||
}
|
||||
|
||||
// Acceso a un enemigo por índice
|
||||
auto EnemyManager::getEnemy(int index) -> std::shared_ptr<Enemy>& {
|
||||
return enemies_.at(index);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Comprueba si hay colisión con algún enemigo
|
||||
auto EnemyManager::checkCollision(SDL_FRect& rect) -> bool {
|
||||
return std::ranges::any_of(enemies_, [&rect](const auto& enemy) {
|
||||
return ::checkCollision(rect, enemy->getCollider());
|
||||
});
|
||||
}
|
||||
52
source/game/gameplay/enemy_manager.hpp
Normal file
52
source/game/gameplay/enemy_manager.hpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "game/entities/enemy.hpp" // Para Enemy, Enemy::Data
|
||||
|
||||
/**
|
||||
* @brief Gestor de enemigos de una habitación
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Almacenar y gestionar la colección de enemigos
|
||||
* - Actualizar todos los enemigos
|
||||
* - Renderizar todos los enemigos
|
||||
* - Detectar colisiones con enemigos
|
||||
*/
|
||||
class EnemyManager {
|
||||
public:
|
||||
EnemyManager() = default;
|
||||
~EnemyManager() = default;
|
||||
|
||||
// Prohibir copia y movimiento para evitar duplicación accidental
|
||||
EnemyManager(const EnemyManager&) = delete;
|
||||
auto operator=(const EnemyManager&) -> EnemyManager& = delete;
|
||||
EnemyManager(EnemyManager&&) = delete;
|
||||
auto operator=(EnemyManager&&) -> EnemyManager& = delete;
|
||||
|
||||
// Gestión de enemigos
|
||||
void addEnemy(std::shared_ptr<Enemy> enemy); // Añade un enemigo a la colección
|
||||
void clear(); // Elimina todos los enemigos
|
||||
void removeLastEnemy(); // Elimina el último enemigo de la colección
|
||||
[[nodiscard]] auto isEmpty() const -> bool; // Comprueba si no hay enemigos
|
||||
|
||||
// Actualización y renderizado
|
||||
void update(float delta_time); // Actualiza todos los enemigos
|
||||
void render(); // Renderiza todos los enemigos
|
||||
|
||||
// Detección de colisiones
|
||||
auto checkCollision(SDL_FRect& rect) -> bool; // Comprueba si hay colisión con algún enemigo
|
||||
|
||||
#ifdef _DEBUG
|
||||
void updateAnimations(float delta_time); // Solo actualiza animaciones sin mover enemigos
|
||||
void resetPositions(const std::vector<Enemy::Data>& enemy_data); // Resetea todos los enemigos a su posición inicial
|
||||
[[nodiscard]] auto getCount() const -> int; // Número de enemigos
|
||||
auto getEnemy(int index) -> std::shared_ptr<Enemy>&; // Acceso a un enemigo por índice
|
||||
#endif
|
||||
|
||||
private:
|
||||
std::vector<std::shared_ptr<Enemy>> enemies_; // Colección de enemigos
|
||||
};
|
||||
69
source/game/gameplay/item_manager.cpp
Normal file
69
source/game/gameplay/item_manager.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
#include "item_manager.hpp"
|
||||
|
||||
#include "core/audio/audio.hpp" // Para Audio
|
||||
#include "game/entities/item.hpp" // Para Item
|
||||
#include "game/options.hpp" // Para Options
|
||||
#include "item_tracker.hpp" // Para ItemTracker
|
||||
#include "scoreboard.hpp" // Para Scoreboard::Data
|
||||
#include "utils/utils.hpp" // Para checkCollision
|
||||
|
||||
// Constructor
|
||||
ItemManager::ItemManager(std::string room_name, std::shared_ptr<Scoreboard::Data> scoreboard_data)
|
||||
: room_name_(std::move(room_name)),
|
||||
data_(std::move(scoreboard_data)) {
|
||||
}
|
||||
|
||||
// Añade un item a la colección
|
||||
void ItemManager::addItem(std::shared_ptr<Item> item) { // NOLINT(readability-identifier-naming)
|
||||
items_.push_back(std::move(item));
|
||||
}
|
||||
|
||||
// Elimina todos los items
|
||||
void ItemManager::clear() {
|
||||
items_.clear();
|
||||
}
|
||||
|
||||
// Actualiza todos los items
|
||||
void ItemManager::update(float delta_time) {
|
||||
for (const auto& item : items_) {
|
||||
item->update(delta_time);
|
||||
}
|
||||
}
|
||||
|
||||
// Renderiza todos los items
|
||||
void ItemManager::render() {
|
||||
for (const auto& item : items_) {
|
||||
item->render();
|
||||
}
|
||||
}
|
||||
|
||||
// Pausa/despausa todos los items
|
||||
void ItemManager::setPaused(bool paused) {
|
||||
for (const auto& item : items_) {
|
||||
item->setPaused(paused);
|
||||
}
|
||||
}
|
||||
|
||||
// Comprueba si hay colisión con algún item
|
||||
auto ItemManager::checkCollision(SDL_FRect& rect) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
for (int i = 0; i < static_cast<int>(items_.size()); ++i) {
|
||||
if (::checkCollision(rect, items_.at(i)->getCollider())) {
|
||||
// Registra el item como recogido
|
||||
ItemTracker::get()->addItem(room_name_, items_.at(i)->getPos());
|
||||
|
||||
// Elimina el item de la colección
|
||||
items_.erase(items_.begin() + i);
|
||||
|
||||
// Reproduce el sonido de pickup
|
||||
Audio::get()->playSound("item.wav", Audio::Group::GAME);
|
||||
|
||||
// Actualiza el scoreboard y estadísticas
|
||||
data_->items++;
|
||||
Options::stats.items = data_->items;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
73
source/game/gameplay/item_manager.hpp
Normal file
73
source/game/gameplay/item_manager.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "game/entities/item.hpp" // Para Item, Item::Data
|
||||
#include "scoreboard.hpp" // Para Scoreboard::Data
|
||||
|
||||
/**
|
||||
* @brief Gestor de items de una habitación
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Almacenar y gestionar la colección de items
|
||||
* - Actualizar todos los items
|
||||
* - Renderizar todos los items
|
||||
* - Detectar colisiones con items y gestionar pickup
|
||||
* - Integración con ItemTracker, Scoreboard y Audio
|
||||
*/
|
||||
class ItemManager {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor
|
||||
* @param room_name Nombre de la habitación (para ItemTracker)
|
||||
* @param scoreboard_data Puntero compartido a los datos del scoreboard
|
||||
*/
|
||||
ItemManager(std::string room_name, std::shared_ptr<Scoreboard::Data> scoreboard_data);
|
||||
~ItemManager() = default;
|
||||
|
||||
// Prohibir copia y movimiento para evitar duplicación accidental
|
||||
ItemManager(const ItemManager&) = delete;
|
||||
auto operator=(const ItemManager&) -> ItemManager& = delete;
|
||||
ItemManager(ItemManager&&) = delete;
|
||||
auto operator=(ItemManager&&) -> ItemManager& = delete;
|
||||
|
||||
// Gestión de items
|
||||
void addItem(std::shared_ptr<Item> item); // Añade un item a la colección
|
||||
void clear(); // Elimina todos los items
|
||||
|
||||
// Actualización y renderizado
|
||||
void update(float delta_time); // Actualiza todos los items
|
||||
void render(); // Renderiza todos los items
|
||||
|
||||
// Estado
|
||||
void setPaused(bool paused); // Pausa/despausa todos los items
|
||||
|
||||
#ifdef _DEBUG
|
||||
[[nodiscard]] auto getCount() const -> int { return static_cast<int>(items_.size()); } // Número de items
|
||||
auto getItem(int index) -> std::shared_ptr<Item>& { return items_.at(index); } // Acceso a un item por índice
|
||||
#endif
|
||||
|
||||
// Detección de colisiones
|
||||
/**
|
||||
* @brief Comprueba si hay colisión con algún item
|
||||
*
|
||||
* Si hay colisión:
|
||||
* - Añade el item a ItemTracker
|
||||
* - Elimina el item de la colección
|
||||
* - Reproduce el sonido de pickup
|
||||
* - Actualiza el scoreboard y estadísticas
|
||||
*
|
||||
* @param rect Rectángulo de colisión
|
||||
* @return true si hubo colisión, false en caso contrario
|
||||
*/
|
||||
auto checkCollision(SDL_FRect& rect) -> bool;
|
||||
|
||||
private:
|
||||
std::vector<std::shared_ptr<Item>> items_; // Colección de items
|
||||
std::string room_name_; // Nombre de la habitación
|
||||
std::shared_ptr<Scoreboard::Data> data_; // Datos del scoreboard
|
||||
};
|
||||
75
source/game/gameplay/item_tracker.cpp
Normal file
75
source/game/gameplay/item_tracker.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "game/gameplay/item_tracker.hpp"
|
||||
|
||||
// [SINGLETON]
|
||||
ItemTracker* ItemTracker::item_tracker = nullptr;
|
||||
|
||||
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||
void ItemTracker::init() {
|
||||
ItemTracker::item_tracker = new ItemTracker();
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
void ItemTracker::destroy() {
|
||||
delete ItemTracker::item_tracker;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto ItemTracker::get() -> ItemTracker* {
|
||||
return ItemTracker::item_tracker;
|
||||
}
|
||||
|
||||
// Comprueba si el objeto ya ha sido cogido
|
||||
auto ItemTracker::hasBeenPicked(const std::string& name, SDL_FPoint pos) -> bool {
|
||||
// Primero busca si ya hay una entrada con ese nombre
|
||||
if (const int INDEX = findByName(name); INDEX != NOT_FOUND) {
|
||||
// Luego busca si existe ya una entrada con esa posición
|
||||
if (findByPos(INDEX, pos) != NOT_FOUND) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Añade el objeto a la lista de objetos cogidos
|
||||
void ItemTracker::addItem(const std::string& name, SDL_FPoint pos) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Comprueba si el objeto no ha sido recogido con anterioridad
|
||||
if (!hasBeenPicked(name, pos)) {
|
||||
// Primero busca si ya hay una entrada con ese nombre
|
||||
if (const int INDEX = findByName(name); INDEX != NOT_FOUND) {
|
||||
items_.at(INDEX).pos.push_back(pos);
|
||||
}
|
||||
// En caso contrario crea la entrada
|
||||
else {
|
||||
items_.emplace_back(name, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Busca una entrada en la lista por nombre
|
||||
auto ItemTracker::findByName(const std::string& name) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int i = 0;
|
||||
|
||||
for (const auto& item : items_) {
|
||||
if (item.name == name) {
|
||||
return i;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return NOT_FOUND;
|
||||
}
|
||||
|
||||
// Busca una entrada en la lista por posición
|
||||
auto ItemTracker::findByPos(int index, SDL_FPoint pos) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
int i = 0;
|
||||
|
||||
for (const auto& item : items_[index].pos) {
|
||||
if ((item.x == pos.x) && (item.y == pos.y)) {
|
||||
return i;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return NOT_FOUND;
|
||||
}
|
||||
49
source/game/gameplay/item_tracker.hpp
Normal file
49
source/game/gameplay/item_tracker.hpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string> // Para string, basic_string
|
||||
#include <utility>
|
||||
#include <vector> // Para vector
|
||||
|
||||
class ItemTracker {
|
||||
public:
|
||||
// Gestión singleton
|
||||
static void init(); // Inicialización
|
||||
static void destroy(); // Destrucción
|
||||
static auto get() -> ItemTracker*; // Acceso al singleton
|
||||
|
||||
// Gestión de items
|
||||
auto hasBeenPicked(const std::string& name, SDL_FPoint pos) -> bool; // Comprueba si el objeto ya ha sido cogido
|
||||
void addItem(const std::string& name, SDL_FPoint pos); // Añade el objeto a la lista
|
||||
|
||||
private:
|
||||
// Tipos anidados privados
|
||||
struct Data {
|
||||
std::string name; // Nombre de la habitación donde se encuentra el objeto
|
||||
std::vector<SDL_FPoint> pos; // Lista de objetos cogidos de la habitación
|
||||
|
||||
// Constructor para facilitar creación con posición inicial
|
||||
Data(std::string name, const SDL_FPoint& position)
|
||||
: name(std::move(name)) {
|
||||
pos.push_back(position);
|
||||
}
|
||||
};
|
||||
|
||||
// Constantes privadas
|
||||
static constexpr int NOT_FOUND = -1; // Valor de retorno cuando no se encuentra un elemento
|
||||
|
||||
// Constantes singleton
|
||||
static ItemTracker* item_tracker; // [SINGLETON] Objeto privado
|
||||
|
||||
// Métodos privados
|
||||
auto findByName(const std::string& name) -> int; // Busca una entrada en la lista por nombre
|
||||
auto findByPos(int index, SDL_FPoint pos) -> int; // Busca una entrada en la lista por posición
|
||||
|
||||
// Constructor y destructor privados [SINGLETON]
|
||||
ItemTracker() = default;
|
||||
~ItemTracker() = default;
|
||||
|
||||
// Variables miembro
|
||||
std::vector<Data> items_; // Lista con todos los objetos recogidos
|
||||
};
|
||||
301
source/game/gameplay/room.cpp
Normal file
301
source/game/gameplay/room.cpp
Normal file
@@ -0,0 +1,301 @@
|
||||
#include "game/gameplay/room.hpp"
|
||||
|
||||
#include <utility> // Para std::move
|
||||
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "game/defaults.hpp" // Para Defaults::Game
|
||||
#include "game/gameplay/collision_map.hpp" // Para CollisionMap
|
||||
#include "game/gameplay/enemy_manager.hpp" // Para EnemyManager
|
||||
#include "game/gameplay/item_manager.hpp" // Para ItemManager
|
||||
#include "game/gameplay/item_tracker.hpp" // Para ItemTracker
|
||||
#include "game/gameplay/room_loader.hpp" // Para RoomLoader
|
||||
#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data
|
||||
#include "game/gameplay/tilemap_renderer.hpp" // Para TilemapRenderer
|
||||
#include "utils/defines.hpp" // Para TILE_SIZE
|
||||
#include "utils/utils.hpp" // Para stringToColor
|
||||
|
||||
// Constructor
|
||||
Room::Room(const std::string& room_path, std::shared_ptr<Scoreboard::Data> data)
|
||||
: data_(std::move(data)) {
|
||||
auto room = Resource::Cache::get()->getRoom(room_path);
|
||||
|
||||
// Crea los managers de enemigos e items
|
||||
enemy_manager_ = std::make_unique<EnemyManager>();
|
||||
item_manager_ = std::make_unique<ItemManager>(room->name, data_);
|
||||
|
||||
initializeRoom(*room);
|
||||
openTheJail(); // Abre la Jail si se da el caso
|
||||
|
||||
// Crea el mapa de colisiones (necesita tile_map_, tile_set_width_, conveyor_belt_direction_)
|
||||
collision_map_ = std::make_unique<CollisionMap>(tile_map_, tile_set_width_, conveyor_belt_direction_);
|
||||
|
||||
// Crea el renderizador del tilemap (necesita tile_map_, tile_set_width_, surface_, bg_color_, conveyor_belt_direction_)
|
||||
tilemap_renderer_ = std::make_unique<TilemapRenderer>(tile_map_, tile_set_width_, surface_, bg_color_, conveyor_belt_direction_);
|
||||
tilemap_renderer_->initialize(collision_map_.get()); // Inicializa (crea map_surface, pinta tiles, busca animados)
|
||||
|
||||
Screen::get()->setBorderColor(stringToColor(border_color_)); // Establece el color del borde
|
||||
}
|
||||
|
||||
// Destructor
|
||||
Room::~Room() = default;
|
||||
|
||||
void Room::initializeRoom(const Data& room) {
|
||||
// Asignar valores a las variables miembro
|
||||
number_ = room.number;
|
||||
name_ = room.name;
|
||||
bg_color_ = room.bg_color;
|
||||
border_color_ = room.border_color;
|
||||
item_color1_ = room.item_color1.empty() ? "yellow" : room.item_color1;
|
||||
item_color2_ = room.item_color2.empty() ? "magenta" : room.item_color2;
|
||||
upper_room_ = room.upper_room;
|
||||
lower_room_ = room.lower_room;
|
||||
left_room_ = room.left_room;
|
||||
right_room_ = room.right_room;
|
||||
tile_set_file_ = room.tile_set_file;
|
||||
conveyor_belt_direction_ = room.conveyor_belt_direction;
|
||||
tile_map_ = room.tile_map; // Tilemap viene embebido en el YAML
|
||||
surface_ = Resource::Cache::get()->getSurface(room.tile_set_file);
|
||||
tile_set_width_ = surface_->getWidth() / TILE_SIZE;
|
||||
is_paused_ = false;
|
||||
|
||||
// Crear los enemigos usando el manager
|
||||
for (const auto& enemy_data : room.enemies) {
|
||||
enemy_manager_->addEnemy(std::make_shared<Enemy>(enemy_data));
|
||||
}
|
||||
|
||||
// Crear los items usando el manager
|
||||
for (const auto& item : room.items) {
|
||||
const SDL_FPoint ITEM_POS = {item.x, item.y};
|
||||
|
||||
if (!ItemTracker::get()->hasBeenPicked(room.name, ITEM_POS)) {
|
||||
// Crear una copia local de los datos del item
|
||||
Item::Data item_copy = item;
|
||||
item_copy.color1 = stringToColor(item_color1_);
|
||||
item_copy.color2 = stringToColor(item_color2_);
|
||||
|
||||
// Crear el objeto Item usando la copia modificada
|
||||
item_manager_->addItem(std::make_shared<Item>(item_copy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Abre la jail para poder entrar
|
||||
void Room::openTheJail() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (data_->jail_is_open && number_ == Defaults::Game::Room::END_ROOM) {
|
||||
// Elimina el último enemigo (Bry debe ser el último enemigo definido en el fichero)
|
||||
if (!enemy_manager_->isEmpty()) {
|
||||
enemy_manager_->removeLastEnemy();
|
||||
}
|
||||
|
||||
// Abre las puertas
|
||||
constexpr int TILE_A = 16 + (13 * 32);
|
||||
constexpr int TILE_B = 16 + (14 * 32);
|
||||
if (TILE_A < tile_map_.size()) {
|
||||
tile_map_[TILE_A] = -1;
|
||||
}
|
||||
if (TILE_B < tile_map_.size()) {
|
||||
tile_map_[TILE_B] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dibuja el mapa en pantalla
|
||||
void Room::renderMap() {
|
||||
tilemap_renderer_->render();
|
||||
}
|
||||
|
||||
// Dibuja los enemigos en pantalla
|
||||
void Room::renderEnemies() {
|
||||
enemy_manager_->render();
|
||||
}
|
||||
|
||||
// Dibuja los objetos en pantalla
|
||||
void Room::renderItems() {
|
||||
item_manager_->render();
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Redibuja el mapa (para actualizar modo debug)
|
||||
void Room::redrawMap() {
|
||||
tilemap_renderer_->redrawMap(collision_map_.get());
|
||||
}
|
||||
|
||||
// Actualiza animaciones sin mover enemigos (para editor de mapas)
|
||||
void Room::updateEditorMode(float delta_time) {
|
||||
tilemap_renderer_->update(delta_time);
|
||||
enemy_manager_->updateAnimations(delta_time);
|
||||
item_manager_->update(delta_time);
|
||||
}
|
||||
|
||||
// Resetea enemigos a posiciones iniciales (para editor de mapas)
|
||||
void Room::resetEnemyPositions(const std::vector<Enemy::Data>& enemy_data) {
|
||||
enemy_manager_->resetPositions(enemy_data);
|
||||
}
|
||||
|
||||
// Cambia un tile y repinta la celda (para editor)
|
||||
void Room::setTile(int index, int tile_value) {
|
||||
if (index >= 0 && index < static_cast<int>(tile_map_.size())) {
|
||||
tile_map_[index] = tile_value;
|
||||
tilemap_renderer_->setTile(index, tile_value);
|
||||
}
|
||||
}
|
||||
|
||||
// Cambia color de fondo y redibuja el mapa (para editor)
|
||||
void Room::setBgColor(const std::string& color) {
|
||||
bg_color_ = color;
|
||||
tilemap_renderer_->setBgColor(color);
|
||||
tilemap_renderer_->redrawMap(collision_map_.get());
|
||||
}
|
||||
|
||||
// Cambia colores de items en vivo (para editor)
|
||||
void Room::setItemColors(const std::string& color1, const std::string& color2) {
|
||||
item_color1_ = color1;
|
||||
item_color2_ = color2;
|
||||
Uint8 c1 = stringToColor(color1);
|
||||
Uint8 c2 = stringToColor(color2);
|
||||
auto* item_mgr = item_manager_.get();
|
||||
for (int i = 0; i < item_mgr->getCount(); ++i) {
|
||||
item_mgr->getItem(i)->setColors(c1, c2);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Actualiza las variables y objetos de la habitación
|
||||
void Room::update(float delta_time) { // NOLINT(readability-make-member-function-const)
|
||||
if (is_paused_) {
|
||||
// Si está en modo pausa no se actualiza nada
|
||||
return;
|
||||
}
|
||||
|
||||
// Actualiza los tiles animados usando el renderer
|
||||
tilemap_renderer_->update(delta_time);
|
||||
|
||||
// Actualiza los enemigos usando el manager
|
||||
enemy_manager_->update(delta_time);
|
||||
|
||||
// Actualiza los items usando el manager
|
||||
item_manager_->update(delta_time);
|
||||
}
|
||||
|
||||
// Pone el mapa en modo pausa
|
||||
void Room::setPaused(bool value) {
|
||||
is_paused_ = value;
|
||||
tilemap_renderer_->setPaused(value);
|
||||
item_manager_->setPaused(value);
|
||||
}
|
||||
|
||||
// Devuelve la cadena del fichero de la habitación contigua segun el borde
|
||||
auto Room::getRoom(Border border) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
switch (border) {
|
||||
case Border::TOP:
|
||||
return upper_room_;
|
||||
case Border::BOTTOM:
|
||||
return lower_room_;
|
||||
case Border::RIGHT:
|
||||
return right_room_;
|
||||
case Border::LEFT:
|
||||
return left_room_;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Devuelve el tipo de tile que hay en ese pixel
|
||||
auto Room::getTile(SDL_FPoint point) -> Tile { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Delega a CollisionMap y convierte el resultado
|
||||
const auto COLLISION_TILE = collision_map_->getTile(point);
|
||||
return static_cast<Tile>(COLLISION_TILE);
|
||||
}
|
||||
|
||||
// Devuelve el tipo de tile en un índice del tilemap
|
||||
auto Room::getTile(int index) -> Tile { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Delega a CollisionMap y convierte el resultado
|
||||
const auto COLLISION_TILE = collision_map_->getTile(index);
|
||||
return static_cast<Tile>(COLLISION_TILE);
|
||||
}
|
||||
|
||||
// Indica si hay colision con un enemigo a partir de un rectangulo
|
||||
auto Room::enemyCollision(SDL_FRect& rect) -> bool {
|
||||
return enemy_manager_->checkCollision(rect);
|
||||
}
|
||||
|
||||
// Indica si hay colision con un objeto a partir de un rectangulo
|
||||
auto Room::itemCollision(SDL_FRect& rect) -> bool {
|
||||
return item_manager_->checkCollision(rect);
|
||||
}
|
||||
|
||||
// Obten la coordenada de la cuesta a partir de un punto perteneciente a ese tile
|
||||
auto Room::getSlopeHeight(SDL_FPoint p, Tile slope) -> int {
|
||||
// Delega a CollisionMap (método estático)
|
||||
const auto COLLISION_TILE = static_cast<CollisionMap::Tile>(slope);
|
||||
return CollisionMap::getSlopeHeight(p, COLLISION_TILE);
|
||||
}
|
||||
|
||||
// === Métodos de colisión (delegados a CollisionMap) ===
|
||||
|
||||
// Comprueba las colisiones con paredes derechas
|
||||
auto Room::checkRightSurfaces(const SDL_FRect& rect) -> int {
|
||||
return collision_map_->checkRightSurfaces(rect);
|
||||
}
|
||||
|
||||
// Comprueba las colisiones con paredes izquierdas
|
||||
auto Room::checkLeftSurfaces(const SDL_FRect& rect) -> int {
|
||||
return collision_map_->checkLeftSurfaces(rect);
|
||||
}
|
||||
|
||||
// Comprueba las colisiones con techos
|
||||
auto Room::checkTopSurfaces(const SDL_FRect& rect) -> int {
|
||||
return collision_map_->checkTopSurfaces(rect);
|
||||
}
|
||||
|
||||
// Comprueba las colisiones punto con techos
|
||||
auto Room::checkTopSurfaces(const SDL_FPoint& p) -> bool {
|
||||
return collision_map_->checkTopSurfaces(p);
|
||||
}
|
||||
|
||||
// Comprueba las colisiones con suelos
|
||||
auto Room::checkBottomSurfaces(const SDL_FRect& rect) -> int {
|
||||
return collision_map_->checkBottomSurfaces(rect);
|
||||
}
|
||||
|
||||
// Comprueba las colisiones con conveyor belts
|
||||
auto Room::checkAutoSurfaces(const SDL_FRect& rect) -> int {
|
||||
return collision_map_->checkAutoSurfaces(rect);
|
||||
}
|
||||
|
||||
// Comprueba las colisiones punto con conveyor belts
|
||||
auto Room::checkConveyorBelts(const SDL_FPoint& p) -> bool {
|
||||
return collision_map_->checkConveyorBelts(p);
|
||||
}
|
||||
|
||||
// Comprueba las colisiones línea con rampas izquierdas
|
||||
auto Room::checkLeftSlopes(const LineVertical& line) -> int {
|
||||
return collision_map_->checkLeftSlopes(line);
|
||||
}
|
||||
|
||||
// Comprueba las colisiones punto con rampas izquierdas
|
||||
auto Room::checkLeftSlopes(const SDL_FPoint& p) -> bool {
|
||||
return collision_map_->checkLeftSlopes(p);
|
||||
}
|
||||
|
||||
// Comprueba las colisiones línea con rampas derechas
|
||||
auto Room::checkRightSlopes(const LineVertical& line) -> int {
|
||||
return collision_map_->checkRightSlopes(line);
|
||||
}
|
||||
|
||||
// Comprueba las colisiones punto con rampas derechas
|
||||
auto Room::checkRightSlopes(const SDL_FPoint& p) -> bool {
|
||||
return collision_map_->checkRightSlopes(p);
|
||||
}
|
||||
|
||||
// Obtiene puntero a slope en un punto
|
||||
auto Room::getSlopeAtPoint(const SDL_FPoint& p) const -> const LineDiagonal* {
|
||||
return collision_map_->getSlopeAtPoint(p);
|
||||
}
|
||||
|
||||
// Carga una habitación desde un archivo YAML (delegado a RoomLoader)
|
||||
auto Room::loadYAML(const std::string& file_path, bool verbose) -> Data { // NOLINT(readability-convert-member-functions-to-static)
|
||||
return RoomLoader::loadYAML(file_path, verbose);
|
||||
}
|
||||
143
source/game/gameplay/room.hpp
Normal file
143
source/game/gameplay/room.hpp
Normal file
@@ -0,0 +1,143 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "game/entities/enemy.hpp" // Para EnemyData
|
||||
#include "game/entities/item.hpp" // Para ItemData
|
||||
#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data
|
||||
#include "utils/utils.hpp" // Para LineHorizontal, LineDiagonal, LineVertical
|
||||
class Sprite; // lines 12-12
|
||||
class Surface; // lines 13-13
|
||||
class EnemyManager;
|
||||
class ItemManager;
|
||||
class CollisionMap;
|
||||
class TilemapRenderer;
|
||||
|
||||
class Room {
|
||||
public:
|
||||
// -- Enumeraciones y estructuras ---
|
||||
enum class Border : int {
|
||||
TOP = 0,
|
||||
RIGHT = 1,
|
||||
BOTTOM = 2,
|
||||
LEFT = 3,
|
||||
NONE = 4
|
||||
};
|
||||
|
||||
enum class Tile {
|
||||
EMPTY,
|
||||
WALL,
|
||||
PASSABLE,
|
||||
SLOPE_L,
|
||||
SLOPE_R,
|
||||
KILL,
|
||||
ANIMATED
|
||||
};
|
||||
|
||||
struct Data {
|
||||
std::string number; // Numero de la habitación
|
||||
std::string name; // Nombre de la habitación
|
||||
std::string bg_color; // Color de fondo de la habitación
|
||||
std::string border_color; // Color del borde de la pantalla
|
||||
std::string item_color1; // Color 1 para los items de la habitación
|
||||
std::string item_color2; // Color 2 para los items de la habitación
|
||||
std::string upper_room; // Identificador de la habitación que se encuentra arriba
|
||||
std::string lower_room; // Identificador de la habitación que se encuentra abajo
|
||||
std::string left_room; // Identificador de la habitación que se encuentra a la izquierda
|
||||
std::string right_room; // Identificador de la habitación que se encuentra a la derecha
|
||||
std::string tile_set_file; // Imagen con los gráficos para la habitación
|
||||
int conveyor_belt_direction{0}; // Sentido en el que arrastran las superficies automáticas de la habitación
|
||||
std::vector<int> tile_map; // Índice de los tiles a dibujar en la habitación (embebido desde YAML)
|
||||
std::vector<Enemy::Data> enemies; // Listado con los enemigos de la habitación
|
||||
std::vector<Item::Data> items; // Listado con los items que hay en la habitación
|
||||
};
|
||||
|
||||
// Constructor y destructor
|
||||
Room(const std::string& room_path, std::shared_ptr<Scoreboard::Data> data);
|
||||
~Room(); // NOLINT(modernize-use-equals-default, performance-trivially-destructible) -- defined in .cpp for unique_ptr with forward declarations
|
||||
|
||||
// --- Funciones ---
|
||||
[[nodiscard]] auto getNumber() const -> const std::string& { return number_; } // Devuelve el numero de la habitación
|
||||
[[nodiscard]] auto getName() const -> const std::string& { return name_; } // Devuelve el nombre de la habitación
|
||||
[[nodiscard]] auto getBGColor() const -> Uint8 { return stringToColor(bg_color_); } // Devuelve el color de la habitación
|
||||
[[nodiscard]] auto getBorderColor() const -> Uint8 { return stringToColor(border_color_); } // Devuelve el color del borde
|
||||
void renderMap(); // Dibuja el mapa en pantalla
|
||||
void renderEnemies(); // Dibuja los enemigos en pantalla
|
||||
void renderItems(); // Dibuja los objetos en pantalla
|
||||
#ifdef _DEBUG
|
||||
void redrawMap(); // Redibuja el mapa (para actualizar modo debug)
|
||||
void updateEditorMode(float delta_time); // Actualiza animaciones sin mover enemigos (para editor)
|
||||
void resetEnemyPositions(const std::vector<Enemy::Data>& enemy_data); // Resetea enemigos a posiciones iniciales
|
||||
auto getEnemyManager() -> EnemyManager* { return enemy_manager_.get(); } // Acceso al gestor de enemigos (para editor)
|
||||
auto getItemManager() -> ItemManager* { return item_manager_.get(); } // Acceso al gestor de items (para editor)
|
||||
void setBgColor(const std::string& color); // Cambia color de fondo y redibuja (para editor)
|
||||
void setItemColors(const std::string& color1, const std::string& color2); // Cambia colores de items (para editor)
|
||||
void setTile(int index, int tile_value); // Cambia un tile y redibuja (para editor)
|
||||
[[nodiscard]] auto getTileSetFile() const -> const std::string& { return tile_set_file_; }
|
||||
[[nodiscard]] auto getTileSetWidth() const -> int { return tile_set_width_; }
|
||||
#endif
|
||||
void update(float delta_time); // Actualiza las variables y objetos de la habitación
|
||||
auto getRoom(Border border) -> std::string; // Devuelve la cadena del fichero de la habitación contigua segun el borde
|
||||
auto getTile(SDL_FPoint point) -> Tile; // Devuelve el tipo de tile que hay en ese pixel
|
||||
auto getTile(int index) -> Tile; // Devuelve el tipo de tile en un índice del tilemap
|
||||
auto enemyCollision(SDL_FRect& rect) -> bool; // Indica si hay colision con un enemigo a partir de un rectangulo
|
||||
auto itemCollision(SDL_FRect& rect) -> bool; // Indica si hay colision con un objeto a partir de un rectangulo
|
||||
static auto getTileSize() -> int { return TILE_SIZE; } // Obten el tamaño del tile
|
||||
static auto getSlopeHeight(SDL_FPoint p, Tile slope) -> int; // Obten la coordenada de la cuesta a partir de un punto perteneciente a ese tile
|
||||
auto checkRightSurfaces(const SDL_FRect& rect) -> int; // Comprueba las colisiones
|
||||
auto checkLeftSurfaces(const SDL_FRect& rect) -> int; // Comprueba las colisiones
|
||||
auto checkTopSurfaces(const SDL_FRect& rect) -> int; // Comprueba las colisiones
|
||||
auto checkBottomSurfaces(const SDL_FRect& rect) -> int; // Comprueba las colisiones
|
||||
auto checkAutoSurfaces(const SDL_FRect& rect) -> int; // Comprueba las colisiones
|
||||
auto checkTopSurfaces(const SDL_FPoint& p) -> bool; // Comprueba las colisiones
|
||||
auto checkConveyorBelts(const SDL_FPoint& p) -> bool; // Comprueba las colisiones
|
||||
auto checkLeftSlopes(const LineVertical& line) -> int; // Comprueba las colisiones
|
||||
auto checkLeftSlopes(const SDL_FPoint& p) -> bool; // Comprueba las colisiones
|
||||
auto checkRightSlopes(const LineVertical& line) -> int; // Comprueba las colisiones
|
||||
auto checkRightSlopes(const SDL_FPoint& p) -> bool; // Comprueba las colisiones
|
||||
[[nodiscard]] auto getSlopeAtPoint(const SDL_FPoint& p) const -> const LineDiagonal*; // Obtiene puntero a slope en un punto
|
||||
void setPaused(bool value); // Pone el mapa en modo pausa
|
||||
[[nodiscard]] auto getConveyorBeltDirection() const -> int { return conveyor_belt_direction_; } // Obten la direccion de las superficies automaticas
|
||||
|
||||
// Método de carga de archivos YAML (delegado a RoomLoader)
|
||||
static auto loadYAML(const std::string& file_path, bool verbose = false) -> Data; // Carga habitación desde archivo YAML unificado
|
||||
|
||||
private:
|
||||
// Constantes
|
||||
static constexpr int TILE_SIZE = 8; // Ancho del tile en pixels
|
||||
static constexpr int MAP_WIDTH = 32; // Ancho del mapa en tiles
|
||||
static constexpr int MAP_HEIGHT = 16; // Alto del mapa en tiles
|
||||
|
||||
// Objetos y punteros
|
||||
std::unique_ptr<EnemyManager> enemy_manager_; // Gestor de enemigos de la habitación
|
||||
std::unique_ptr<ItemManager> item_manager_; // Gestor de items de la habitación
|
||||
std::unique_ptr<CollisionMap> collision_map_; // Mapa de colisiones de la habitación
|
||||
std::unique_ptr<TilemapRenderer> tilemap_renderer_; // Renderizador del mapa de tiles
|
||||
std::shared_ptr<Surface> surface_; // Textura con los graficos de la habitación
|
||||
std::shared_ptr<Scoreboard::Data> data_; // Puntero a los datos del marcador
|
||||
|
||||
// --- Variables ---
|
||||
std::string number_; // Numero de la habitación
|
||||
std::string name_; // Nombre de la habitación
|
||||
std::string bg_color_; // Color de fondo de la habitación
|
||||
std::string border_color_; // Color del borde de la pantalla
|
||||
std::string item_color1_; // Color 1 para los items de la habitación
|
||||
std::string item_color2_; // Color 2 para los items de la habitación
|
||||
std::string upper_room_; // Identificador de la habitación que se encuentra arriba
|
||||
std::string lower_room_; // Identificador de la habitación que se encuentra abajp
|
||||
std::string left_room_; // Identificador de la habitación que se encuentra a la izquierda
|
||||
std::string right_room_; // Identificador de la habitación que se encuentra a la derecha
|
||||
std::string tile_set_file_; // Imagen con los graficos para la habitación
|
||||
std::vector<int> tile_map_; // Indice de los tiles a dibujar en la habitación (embebido desde YAML)
|
||||
int conveyor_belt_direction_{0}; // Sentido en el que arrastran las superficies automáticas de la habitación
|
||||
bool is_paused_{false}; // Indica si el mapa esta en modo pausa
|
||||
int tile_set_width_{0}; // Ancho del tileset en tiles
|
||||
|
||||
// --- Funciones ---
|
||||
void initializeRoom(const Data& room); // Inicializa los valores
|
||||
void openTheJail(); // Abre la jail para poder entrar
|
||||
};
|
||||
375
source/game/gameplay/room_loader.cpp
Normal file
375
source/game/gameplay/room_loader.cpp
Normal file
@@ -0,0 +1,375 @@
|
||||
#include "room_loader.hpp"
|
||||
|
||||
#include <exception> // Para exception
|
||||
#include <iostream> // Para cout, cerr
|
||||
|
||||
#include "core/resources/resource_helper.hpp" // Para Resource::Helper
|
||||
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||
#include "game/options.hpp" // Para Options::language
|
||||
#include "utils/defines.hpp" // Para Tile::SIZE
|
||||
#include "utils/utils.hpp" // Para stringToColor
|
||||
|
||||
// Convierte room connection de YAML a formato interno
|
||||
auto RoomLoader::convertRoomConnection(const std::string& value) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (value == "null" || value.empty()) {
|
||||
return "0";
|
||||
}
|
||||
// Si ya tiene .yaml, devolverlo tal cual; si no, añadirlo
|
||||
if (value.size() > 5 && value.substr(value.size() - 5) == ".yaml") {
|
||||
return value;
|
||||
}
|
||||
return value + ".yaml";
|
||||
}
|
||||
|
||||
// Convierte string de autoSurface a int
|
||||
auto RoomLoader::convertAutoSurface(const fkyaml::node& node) -> int { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (node.is_integer()) {
|
||||
return node.get_value<int>();
|
||||
}
|
||||
if (node.is_string()) {
|
||||
const auto VALUE = node.get_value<std::string>();
|
||||
if (VALUE == "left") {
|
||||
return -1;
|
||||
}
|
||||
if (VALUE == "right") {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0; // "none" o default
|
||||
}
|
||||
|
||||
// Convierte un tilemap 2D a vector 1D flat
|
||||
auto RoomLoader::flattenTilemap(const std::vector<std::vector<int>>& tilemap_2d) -> std::vector<int> { // NOLINT(readability-convert-member-functions-to-static, readability-named-parameter)
|
||||
std::vector<int> tilemap_flat;
|
||||
tilemap_flat.reserve(512); // 16 rows × 32 cols
|
||||
|
||||
for (const auto& row : tilemap_2d) {
|
||||
for (int tile : row) {
|
||||
tilemap_flat.push_back(tile);
|
||||
}
|
||||
}
|
||||
|
||||
return tilemap_flat;
|
||||
}
|
||||
|
||||
// Parsea la configuración general de la habitación
|
||||
void RoomLoader::parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (!yaml.contains("room")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& room_node = yaml["room"];
|
||||
|
||||
// Extract room number from filename (e.g., "01.yaml" → "01")
|
||||
room.number = file_name.substr(0, file_name.find_last_of('.'));
|
||||
|
||||
// Basic properties
|
||||
const std::string LANG_KEY = "name_" + Options::language;
|
||||
if (room_node.contains(LANG_KEY)) {
|
||||
room.name = room_node[LANG_KEY].get_value<std::string>();
|
||||
} else if (room_node.contains("name")) {
|
||||
room.name = room_node["name"].get_value<std::string>();
|
||||
}
|
||||
if (room_node.contains("bgColor")) {
|
||||
room.bg_color = room_node["bgColor"].get_value<std::string>();
|
||||
}
|
||||
if (room_node.contains("border")) {
|
||||
room.border_color = room_node["border"].get_value<std::string>();
|
||||
}
|
||||
if (room_node.contains("tileSetFile")) {
|
||||
room.tile_set_file = room_node["tileSetFile"].get_value<std::string>();
|
||||
}
|
||||
|
||||
// Room connections
|
||||
if (room_node.contains("connections")) {
|
||||
parseRoomConnections(room_node["connections"], room);
|
||||
}
|
||||
|
||||
// Item colors
|
||||
room.item_color1 = room_node.contains("itemColor1")
|
||||
? room_node["itemColor1"].get_value_or<std::string>("yellow")
|
||||
: "yellow";
|
||||
|
||||
room.item_color2 = room_node.contains("itemColor2")
|
||||
? room_node["itemColor2"].get_value_or<std::string>("magenta")
|
||||
: "magenta";
|
||||
|
||||
// Dirección de la cinta transportadora (left/none/right)
|
||||
room.conveyor_belt_direction = room_node.contains("conveyorBelt")
|
||||
? convertAutoSurface(room_node["conveyorBelt"])
|
||||
: 0;
|
||||
}
|
||||
|
||||
// Parsea las conexiones de la habitación (arriba/abajo/izq/der)
|
||||
void RoomLoader::parseRoomConnections(const fkyaml::node& conn_node, Room::Data& room) {
|
||||
room.upper_room = conn_node.contains("up")
|
||||
? convertRoomConnection(conn_node["up"].get_value_or<std::string>("null"))
|
||||
: "0";
|
||||
|
||||
room.lower_room = conn_node.contains("down")
|
||||
? convertRoomConnection(conn_node["down"].get_value_or<std::string>("null"))
|
||||
: "0";
|
||||
|
||||
room.left_room = conn_node.contains("left")
|
||||
? convertRoomConnection(conn_node["left"].get_value_or<std::string>("null"))
|
||||
: "0";
|
||||
|
||||
room.right_room = conn_node.contains("right")
|
||||
? convertRoomConnection(conn_node["right"].get_value_or<std::string>("null"))
|
||||
: "0";
|
||||
}
|
||||
|
||||
// Parsea el tilemap de la habitación
|
||||
void RoomLoader::parseTilemap(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (!yaml.contains("tilemap")) {
|
||||
std::cerr << "Warning: No tilemap found in " << file_name << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& tilemap_node = yaml["tilemap"];
|
||||
|
||||
// Read 2D array
|
||||
std::vector<std::vector<int>> tilemap_2d;
|
||||
tilemap_2d.reserve(16);
|
||||
|
||||
for (const auto& row_node : tilemap_node) {
|
||||
std::vector<int> row;
|
||||
row.reserve(32);
|
||||
|
||||
for (const auto& tile_node : row_node) {
|
||||
row.push_back(tile_node.get_value<int>());
|
||||
}
|
||||
|
||||
tilemap_2d.push_back(row);
|
||||
}
|
||||
|
||||
// Convert to 1D flat array
|
||||
room.tile_map = flattenTilemap(tilemap_2d);
|
||||
|
||||
if (verbose) {
|
||||
std::cout << "Loaded tilemap: " << room.tile_map.size() << " tiles\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Parsea los límites de movimiento de un enemigo
|
||||
void RoomLoader::parseEnemyBoundaries(const fkyaml::node& bounds_node, Enemy::Data& enemy) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Nuevo formato: position1 y position2
|
||||
if (bounds_node.contains("position1")) {
|
||||
const auto& pos1 = bounds_node["position1"];
|
||||
if (pos1.contains("x")) {
|
||||
enemy.x1 = pos1["x"].get_value<int>() * Tile::SIZE;
|
||||
}
|
||||
if (pos1.contains("y")) {
|
||||
enemy.y1 = pos1["y"].get_value<int>() * Tile::SIZE;
|
||||
}
|
||||
}
|
||||
if (bounds_node.contains("position2")) {
|
||||
const auto& pos2 = bounds_node["position2"];
|
||||
if (pos2.contains("x")) {
|
||||
enemy.x2 = pos2["x"].get_value<int>() * Tile::SIZE;
|
||||
}
|
||||
if (pos2.contains("y")) {
|
||||
enemy.y2 = pos2["y"].get_value<int>() * Tile::SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
// Formato antiguo: x1/y1/x2/y2 (compatibilidad)
|
||||
if (bounds_node.contains("x1")) {
|
||||
enemy.x1 = bounds_node["x1"].get_value<int>() * Tile::SIZE;
|
||||
}
|
||||
if (bounds_node.contains("y1")) {
|
||||
enemy.y1 = bounds_node["y1"].get_value<int>() * Tile::SIZE;
|
||||
}
|
||||
if (bounds_node.contains("x2")) {
|
||||
enemy.x2 = bounds_node["x2"].get_value<int>() * Tile::SIZE;
|
||||
}
|
||||
if (bounds_node.contains("y2")) {
|
||||
enemy.y2 = bounds_node["y2"].get_value<int>() * Tile::SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
// Parsea los datos de un enemigo individual
|
||||
auto RoomLoader::parseEnemyData(const fkyaml::node& enemy_node) -> Enemy::Data { // NOLINT(readability-convert-member-functions-to-static)
|
||||
Enemy::Data enemy;
|
||||
|
||||
// Animation path
|
||||
if (enemy_node.contains("animation")) {
|
||||
enemy.animation_path = enemy_node["animation"].get_value<std::string>();
|
||||
}
|
||||
|
||||
// Position (in tiles, convert to pixels)
|
||||
if (enemy_node.contains("position")) {
|
||||
const auto& pos = enemy_node["position"];
|
||||
if (pos.contains("x")) {
|
||||
enemy.x = pos["x"].get_value<float>() * Tile::SIZE;
|
||||
}
|
||||
if (pos.contains("y")) {
|
||||
enemy.y = pos["y"].get_value<float>() * Tile::SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
// Velocity (already in pixels/second)
|
||||
if (enemy_node.contains("velocity")) {
|
||||
const auto& vel = enemy_node["velocity"];
|
||||
if (vel.contains("x")) {
|
||||
enemy.vx = vel["x"].get_value<float>();
|
||||
}
|
||||
if (vel.contains("y")) {
|
||||
enemy.vy = vel["y"].get_value<float>();
|
||||
}
|
||||
}
|
||||
|
||||
// Boundaries (in tiles, convert to pixels)
|
||||
if (enemy_node.contains("boundaries")) {
|
||||
parseEnemyBoundaries(enemy_node["boundaries"], enemy);
|
||||
}
|
||||
|
||||
// Color
|
||||
enemy.color = enemy_node.contains("color")
|
||||
? enemy_node["color"].get_value_or<std::string>("white")
|
||||
: "white";
|
||||
|
||||
// Optional fields
|
||||
enemy.flip = enemy_node.contains("flip")
|
||||
? enemy_node["flip"].get_value_or<bool>(false)
|
||||
: false;
|
||||
|
||||
enemy.mirror = enemy_node.contains("mirror")
|
||||
? enemy_node["mirror"].get_value_or<bool>(false)
|
||||
: false;
|
||||
|
||||
enemy.frame = enemy_node.contains("frame")
|
||||
? enemy_node["frame"].get_value_or<int>(-1)
|
||||
: -1;
|
||||
|
||||
return enemy;
|
||||
}
|
||||
|
||||
// Parsea la lista de enemigos de la habitación
|
||||
void RoomLoader::parseEnemies(const fkyaml::node& yaml, Room::Data& room, bool verbose) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (!yaml.contains("enemies") || yaml["enemies"].is_null()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& enemies_node = yaml["enemies"];
|
||||
|
||||
for (const auto& enemy_node : enemies_node) {
|
||||
room.enemies.push_back(parseEnemyData(enemy_node));
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
std::cout << "Loaded " << room.enemies.size() << " enemies\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Parsea los datos de un item individual
|
||||
auto RoomLoader::parseItemData(const fkyaml::node& item_node, const Room::Data& room) -> Item::Data { // NOLINT(readability-convert-member-functions-to-static)
|
||||
Item::Data item;
|
||||
|
||||
// Tileset file
|
||||
if (item_node.contains("tileSetFile")) {
|
||||
item.tile_set_file = item_node["tileSetFile"].get_value<std::string>();
|
||||
}
|
||||
|
||||
// Tile index
|
||||
if (item_node.contains("tile")) {
|
||||
item.tile = item_node["tile"].get_value<int>();
|
||||
}
|
||||
|
||||
// Position (in tiles, convert to pixels)
|
||||
if (item_node.contains("position")) {
|
||||
const auto& pos = item_node["position"];
|
||||
if (pos.contains("x")) {
|
||||
item.x = pos["x"].get_value<float>() * Tile::SIZE;
|
||||
}
|
||||
if (pos.contains("y")) {
|
||||
item.y = pos["y"].get_value<float>() * Tile::SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
// Counter
|
||||
item.counter = item_node.contains("counter")
|
||||
? item_node["counter"].get_value_or<int>(0)
|
||||
: 0;
|
||||
|
||||
// Colors (assigned from room defaults)
|
||||
item.color1 = stringToColor(room.item_color1);
|
||||
item.color2 = stringToColor(room.item_color2);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
// Parsea la lista de items de la habitación
|
||||
void RoomLoader::parseItems(const fkyaml::node& yaml, Room::Data& room, bool verbose) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
if (!yaml.contains("items") || yaml["items"].is_null()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& items_node = yaml["items"];
|
||||
|
||||
for (const auto& item_node : items_node) {
|
||||
room.items.push_back(parseItemData(item_node, room));
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
std::cout << "Loaded " << room.items.size() << " items\n";
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Carga una habitación desde un string YAML (para el editor de mapas, evita el resource pack)
|
||||
auto RoomLoader::loadFromString(const std::string& yaml_content, const std::string& file_name) -> Room::Data {
|
||||
Room::Data room;
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||
parseRoomConfig(yaml, room, file_name);
|
||||
parseTilemap(yaml, room, file_name, false);
|
||||
parseEnemies(yaml, room, false);
|
||||
parseItems(yaml, room, false);
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "YAML parsing error in " << file_name << ": " << e.what() << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading room " << file_name << ": " << e.what() << '\n';
|
||||
}
|
||||
return room;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Carga un archivo de room en formato YAML
|
||||
auto RoomLoader::loadYAML(const std::string& file_path, bool verbose) -> Room::Data { // NOLINT(readability-convert-member-functions-to-static)
|
||||
Room::Data room;
|
||||
|
||||
// Extract filename for logging
|
||||
const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1);
|
||||
|
||||
try {
|
||||
// Load YAML file using ResourceHelper (supports both filesystem and pack)
|
||||
auto file_data = Resource::Helper::loadFile(file_path);
|
||||
|
||||
if (file_data.empty()) {
|
||||
std::cerr << "Error: Unable to load file " << FILE_NAME << '\n';
|
||||
return room;
|
||||
}
|
||||
|
||||
// Parse YAML from string
|
||||
std::string yaml_content(file_data.begin(), file_data.end());
|
||||
auto yaml = fkyaml::node::deserialize(yaml_content);
|
||||
|
||||
// Delegación a funciones especializadas
|
||||
parseRoomConfig(yaml, room, FILE_NAME);
|
||||
parseTilemap(yaml, room, FILE_NAME, verbose);
|
||||
parseEnemies(yaml, room, verbose);
|
||||
parseItems(yaml, room, verbose);
|
||||
|
||||
if (verbose) {
|
||||
std::cout << "Room loaded successfully: " << FILE_NAME << '\n';
|
||||
}
|
||||
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "YAML parsing error in " << FILE_NAME << ": " << e.what() << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading room " << FILE_NAME << ": " << e.what() << '\n';
|
||||
}
|
||||
|
||||
return room;
|
||||
}
|
||||
136
source/game/gameplay/room_loader.hpp
Normal file
136
source/game/gameplay/room_loader.hpp
Normal file
@@ -0,0 +1,136 @@
|
||||
#pragma once
|
||||
|
||||
#include <string> // Para string
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||
#include "game/entities/enemy.hpp" // Para Enemy::Data
|
||||
#include "game/entities/item.hpp" // Para Item::Data
|
||||
#include "game/gameplay/room.hpp" // Para Room::Data
|
||||
|
||||
/**
|
||||
* @brief Cargador de archivos de habitaciones en formato YAML
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Cargar archivos de room en formato YAML unificado (.yaml)
|
||||
* - Parsear datos de room, tilemap, enemies e items
|
||||
* - Convertir tipos de datos (tiles, posiciones, colores)
|
||||
* - Validar y propagar errores de carga
|
||||
*
|
||||
* Esta clase contiene solo métodos estáticos y no debe instanciarse.
|
||||
*/
|
||||
class RoomLoader {
|
||||
public:
|
||||
// Constructor eliminado para prevenir instanciación
|
||||
RoomLoader() = delete;
|
||||
~RoomLoader() = delete;
|
||||
RoomLoader(const RoomLoader&) = delete;
|
||||
auto operator=(const RoomLoader&) -> RoomLoader& = delete;
|
||||
RoomLoader(RoomLoader&&) = delete;
|
||||
auto operator=(RoomLoader&&) -> RoomLoader& = delete;
|
||||
|
||||
/**
|
||||
* @brief Carga un archivo de room en formato YAML
|
||||
* @param file_path Ruta al archivo YAML de habitación
|
||||
* @param verbose Si true, muestra información de debug
|
||||
* @return Room::Data con todos los datos de la habitación incluyendo:
|
||||
* - Configuración de la habitación (nombre, colores, etc.)
|
||||
* - Tilemap completo (convertido de 2D a 1D)
|
||||
* - Lista de enemigos con todas sus propiedades
|
||||
* - Lista de items con todas sus propiedades
|
||||
*
|
||||
* El formato YAML esperado incluye:
|
||||
* - room: configuración general
|
||||
* - tilemap: array 2D de 16x32 tiles (convertido a vector 1D de 512 elementos)
|
||||
* - enemies: lista de enemigos (opcional)
|
||||
* - items: lista de items (opcional)
|
||||
*/
|
||||
static auto loadYAML(const std::string& file_path, bool verbose = false) -> Room::Data;
|
||||
#ifdef _DEBUG
|
||||
static auto loadFromString(const std::string& yaml_content, const std::string& file_name) -> Room::Data;
|
||||
#endif
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Convierte room connection de YAML a formato interno
|
||||
* @param value Valor del YAML (vacío, "02" o "02.yaml")
|
||||
* @return "0" para sin conexión, o nombre del archivo con extensión
|
||||
*/
|
||||
static auto convertRoomConnection(const std::string& value) -> std::string;
|
||||
|
||||
/**
|
||||
* @brief Convierte autoSurface de YAML a int
|
||||
* @param node Nodo YAML (puede ser int o string "left"/"none"/"right")
|
||||
* @return -1 para left, 0 para none, 1 para right
|
||||
*/
|
||||
static auto convertAutoSurface(const fkyaml::node& node) -> int;
|
||||
|
||||
/**
|
||||
* @brief Convierte un tilemap 2D a vector 1D flat
|
||||
* @param tilemap_2d Array 2D de tiles (16 rows × 32 cols)
|
||||
* @return Vector 1D flat con 512 elementos
|
||||
*/
|
||||
static auto flattenTilemap(const std::vector<std::vector<int>>& tilemap_2d) -> std::vector<int>; // NOLINT(readability-avoid-const-params-in-decls)
|
||||
|
||||
/**
|
||||
* @brief Parsea la configuración general de la habitación
|
||||
* @param yaml Nodo raíz del YAML
|
||||
* @param room Estructura de datos de la habitación a rellenar
|
||||
* @param file_name Nombre del archivo para logging
|
||||
*/
|
||||
static void parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name);
|
||||
|
||||
/**
|
||||
* @brief Parsea las conexiones de la habitación (arriba/abajo/izq/der)
|
||||
* @param conn_node Nodo YAML con las conexiones
|
||||
* @param room Estructura de datos de la habitación a rellenar
|
||||
*/
|
||||
static void parseRoomConnections(const fkyaml::node& conn_node, Room::Data& room);
|
||||
|
||||
/**
|
||||
* @brief Parsea el tilemap de la habitación
|
||||
* @param yaml Nodo raíz del YAML
|
||||
* @param room Estructura de datos de la habitación a rellenar
|
||||
* @param file_name Nombre del archivo para logging
|
||||
* @param verbose Si true, muestra información de debug
|
||||
*/
|
||||
static void parseTilemap(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose);
|
||||
|
||||
/**
|
||||
* @brief Parsea la lista de enemigos de la habitación
|
||||
* @param yaml Nodo raíz del YAML
|
||||
* @param room Estructura de datos de la habitación a rellenar
|
||||
* @param verbose Si true, muestra información de debug
|
||||
*/
|
||||
static void parseEnemies(const fkyaml::node& yaml, Room::Data& room, bool verbose);
|
||||
|
||||
/**
|
||||
* @brief Parsea los datos de un enemigo individual
|
||||
* @param enemy_node Nodo YAML del enemigo
|
||||
* @return Estructura Enemy::Data con los datos parseados
|
||||
*/
|
||||
static auto parseEnemyData(const fkyaml::node& enemy_node) -> Enemy::Data;
|
||||
|
||||
/**
|
||||
* @brief Parsea los límites de movimiento de un enemigo
|
||||
* @param bounds_node Nodo YAML con los límites
|
||||
* @param enemy Estructura del enemigo a rellenar
|
||||
*/
|
||||
static void parseEnemyBoundaries(const fkyaml::node& bounds_node, Enemy::Data& enemy);
|
||||
|
||||
/**
|
||||
* @brief Parsea la lista de items de la habitación
|
||||
* @param yaml Nodo raíz del YAML
|
||||
* @param room Estructura de datos de la habitación a rellenar
|
||||
* @param verbose Si true, muestra información de debug
|
||||
*/
|
||||
static void parseItems(const fkyaml::node& yaml, Room::Data& room, bool verbose);
|
||||
|
||||
/**
|
||||
* @brief Parsea los datos de un item individual
|
||||
* @param item_node Nodo YAML del item
|
||||
* @param room Datos de la habitación (para colores por defecto)
|
||||
* @return Estructura Item::Data con los datos parseados
|
||||
*/
|
||||
static auto parseItemData(const fkyaml::node& item_node, const Room::Data& room) -> Item::Data;
|
||||
};
|
||||
20
source/game/gameplay/room_tracker.cpp
Normal file
20
source/game/gameplay/room_tracker.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "game/gameplay/room_tracker.hpp"
|
||||
|
||||
#include <algorithm> // Para std::ranges::any_of
|
||||
|
||||
// Comprueba si la habitación ya ha sido visitada
|
||||
auto RoomTracker::hasBeenVisited(const std::string& name) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
return std::ranges::any_of(rooms_, [&name](const auto& l) -> bool { return l == name; });
|
||||
}
|
||||
|
||||
// Añade la habitación a la lista
|
||||
auto RoomTracker::addRoom(const std::string& name) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
// Comprueba si la habitación ya ha sido visitada
|
||||
if (!hasBeenVisited(name)) {
|
||||
// En caso contrario añádela a la lista
|
||||
rooms_.push_back(name);
|
||||
return true; // NOLINT(readability-simplify-boolean-expr)
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
17
source/game/gameplay/room_tracker.hpp
Normal file
17
source/game/gameplay/room_tracker.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <string> // Para string
|
||||
#include <vector> // Para vector
|
||||
|
||||
class RoomTracker {
|
||||
public:
|
||||
RoomTracker() = default; // Constructor
|
||||
~RoomTracker() = default; // Destructor
|
||||
|
||||
auto addRoom(const std::string& name) -> bool; // Añade la habitación a la lista
|
||||
|
||||
private:
|
||||
auto hasBeenVisited(const std::string& name) -> bool; // Comprueba si ya ha sido visitada
|
||||
|
||||
std::vector<std::string> rooms_; // Lista con habitaciones visitadas
|
||||
};
|
||||
183
source/game/gameplay/scoreboard.cpp
Normal file
183
source/game/gameplay/scoreboard.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
#include "game/gameplay/scoreboard.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "core/locale/locale.hpp" // Para Locale
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/sprite/animated_sprite.hpp" // Para SAnimatedSprite
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/rendering/text.hpp" // Para Text
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "game/entities/player.hpp" // Para Player::skinToAnimationPath
|
||||
#include "game/options.hpp" // Para Options, options, Cheat, OptionsGame
|
||||
#include "utils/defines.hpp" // Para BLOCK
|
||||
#include "utils/utils.hpp" // Para stringToColor
|
||||
|
||||
// Constructor
|
||||
Scoreboard::Scoreboard(std::shared_ptr<Data> data)
|
||||
: item_surface_(Resource::Cache::get()->getSurface("items.gif")),
|
||||
data_(std::move(std::move(data))) {
|
||||
const float SURFACE_WIDTH = Options::game.width;
|
||||
constexpr float SURFACE_HEIGHT = 6.0F * Tile::SIZE;
|
||||
|
||||
// Reserva memoria para los objetos
|
||||
const std::string PLAYER_ANIM_PATH = Player::skinToAnimationPath(Options::game.player_skin);
|
||||
const auto& player_animation_data = Resource::Cache::get()->getAnimationData(PLAYER_ANIM_PATH);
|
||||
player_sprite_ = std::make_shared<AnimatedSprite>(player_animation_data);
|
||||
player_sprite_->setCurrentAnimation("default");
|
||||
|
||||
surface_ = std::make_shared<Surface>(SURFACE_WIDTH, SURFACE_HEIGHT);
|
||||
surface_dest_ = {.x = 0, .y = Options::game.height - SURFACE_HEIGHT, .w = SURFACE_WIDTH, .h = SURFACE_HEIGHT};
|
||||
|
||||
// Inicializa el color de items
|
||||
items_color_ = stringToColor("white");
|
||||
|
||||
// Inicializa el vector de colores
|
||||
const std::vector<std::string> COLORS = {"blue", "magenta", "green", "cyan", "yellow", "white", "bright_blue", "bright_magenta", "bright_green", "bright_cyan", "bright_yellow", "bright_white"};
|
||||
for (const auto& color : COLORS) {
|
||||
color_.push_back(stringToColor(color));
|
||||
}
|
||||
}
|
||||
|
||||
// Pinta el objeto en pantalla
|
||||
void Scoreboard::render() {
|
||||
surface_->render(nullptr, &surface_dest_);
|
||||
}
|
||||
|
||||
// Actualiza las variables del objeto
|
||||
void Scoreboard::update(float delta_time) {
|
||||
// Acumular tiempo para animaciones
|
||||
time_accumulator_ += delta_time;
|
||||
|
||||
// Actualiza el color de la cantidad de items recogidos
|
||||
updateItemsColor(delta_time);
|
||||
|
||||
// Dibuja la textura
|
||||
fillTexture();
|
||||
|
||||
if (!is_paused_) {
|
||||
// Si está en pausa no se actualiza el reloj
|
||||
clock_ = getTime();
|
||||
}
|
||||
}
|
||||
|
||||
// Obtiene el tiempo transcurrido de partida
|
||||
auto Scoreboard::getTime() -> Scoreboard::ClockData { // NOLINT(readability-convert-member-functions-to-static)
|
||||
const Uint32 TIME_ELAPSED = SDL_GetTicks() - data_->ini_clock - paused_time_elapsed_;
|
||||
|
||||
ClockData time;
|
||||
time.hours = TIME_ELAPSED / 3600000;
|
||||
time.minutes = TIME_ELAPSED / 60000;
|
||||
time.seconds = TIME_ELAPSED / 1000;
|
||||
time.separator = (TIME_ELAPSED % 1000 <= 500) ? ":" : " ";
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
// Actualiza el sprite del jugador con la skin actual
|
||||
void Scoreboard::refreshPlayerSkin() {
|
||||
const std::string PLAYER_ANIM_PATH = Player::skinToAnimationPath(Options::game.player_skin);
|
||||
const auto& player_animation_data = Resource::Cache::get()->getAnimationData(PLAYER_ANIM_PATH);
|
||||
player_sprite_ = std::make_shared<AnimatedSprite>(player_animation_data);
|
||||
player_sprite_->setCurrentAnimation("default");
|
||||
}
|
||||
|
||||
// Pone el marcador en modo pausa
|
||||
void Scoreboard::setPaused(bool value) {
|
||||
if (is_paused_ == value) {
|
||||
// Evita ejecutar lógica si el estado no cambia
|
||||
return;
|
||||
}
|
||||
|
||||
is_paused_ = value;
|
||||
|
||||
if (is_paused_) {
|
||||
// Guarda el tiempo actual al pausar
|
||||
paused_time_ = SDL_GetTicks();
|
||||
} else {
|
||||
// Calcula el tiempo pausado acumulado al reanudar
|
||||
paused_time_elapsed_ += SDL_GetTicks() - paused_time_;
|
||||
}
|
||||
}
|
||||
|
||||
// Actualiza el color de la cantidad de items recogidos
|
||||
void Scoreboard::updateItemsColor(float delta_time) {
|
||||
if (!data_->jail_is_open) {
|
||||
return;
|
||||
}
|
||||
|
||||
items_color_timer_ += delta_time;
|
||||
|
||||
// Resetear timer cada 2 ciclos (0.666s total)
|
||||
if (items_color_timer_ >= ITEMS_COLOR_BLINK_DURATION * 2.0F) {
|
||||
items_color_timer_ = 0.0F;
|
||||
}
|
||||
|
||||
// Alternar color cada ITEMS_COLOR_BLINK_DURATION
|
||||
if (items_color_timer_ < ITEMS_COLOR_BLINK_DURATION) {
|
||||
items_color_ = stringToColor("white");
|
||||
} else {
|
||||
items_color_ = stringToColor("magenta");
|
||||
}
|
||||
}
|
||||
|
||||
// Devuelve la cantidad de minutos de juego transcurridos
|
||||
auto Scoreboard::getMinutes() -> int {
|
||||
return getTime().minutes;
|
||||
}
|
||||
|
||||
// Dibuja los elementos del marcador en la textura
|
||||
void Scoreboard::fillTexture() {
|
||||
// Empieza a dibujar en la textura
|
||||
auto previuos_renderer = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(surface_);
|
||||
|
||||
// Limpia la textura
|
||||
surface_->clear(stringToColor("black"));
|
||||
|
||||
// Dibuja las vidas
|
||||
const int WALK_FRAMES = player_sprite_->getCurrentAnimationSize();
|
||||
const int DESP = static_cast<int>(time_accumulator_ / SPRITE_WALK_CYCLE_DURATION) % (WALK_FRAMES * 2);
|
||||
const int FRAME = DESP % WALK_FRAMES;
|
||||
player_sprite_->setCurrentAnimationFrame(FRAME);
|
||||
player_sprite_->setPosY(LINE2_Y);
|
||||
for (int i = 0; i < data_->lives; ++i) {
|
||||
player_sprite_->setPosX(LIVES_START_X + (LIVES_SPACING * i) + DESP);
|
||||
const int INDEX = i % color_.size();
|
||||
player_sprite_->render(1, color_.at(INDEX));
|
||||
}
|
||||
|
||||
// Muestra si suena la música
|
||||
if (data_->music) {
|
||||
const Uint8 C = data_->color;
|
||||
SDL_FRect clip = {.x = 0, .y = 8, .w = 8, .h = 8};
|
||||
item_surface_->renderWithColorReplace(MUSIC_ICON_X, LINE2_Y, 1, C, &clip);
|
||||
}
|
||||
|
||||
// Escribe los textos
|
||||
auto text = Resource::Cache::get()->getText("smb2");
|
||||
const std::string TIME_TEXT = std::to_string((clock_.minutes % 100) / 10) + std::to_string(clock_.minutes % 10) + clock_.separator + std::to_string((clock_.seconds % 60) / 10) + std::to_string(clock_.seconds % 10);
|
||||
const std::string ITEMS_TEXT = std::to_string(data_->items / 100) + std::to_string((data_->items % 100) / 10) + std::to_string(data_->items % 10);
|
||||
text->writeColored(ITEMS_LABEL_X, LINE1_Y, Locale::get()->get("scoreboard.items"), data_->color); // NOLINT(readability-static-accessed-through-instance)
|
||||
text->writeColored(ITEMS_VALUE_X, LINE1_Y, ITEMS_TEXT, items_color_);
|
||||
text->writeColored(TIME_LABEL_X, LINE1_Y, Locale::get()->get("scoreboard.time"), data_->color); // NOLINT(readability-static-accessed-through-instance)
|
||||
text->writeColored(TIME_VALUE_X, LINE1_Y, TIME_TEXT, stringToColor("white"));
|
||||
|
||||
const std::string ROOMS_TEXT = std::to_string(data_->rooms / 100) + std::to_string((data_->rooms % 100) / 10) + std::to_string(data_->rooms % 10);
|
||||
text->writeColored(ROOMS_LABEL_X, LINE2_Y, Locale::get()->get("scoreboard.rooms"), stringToColor("white")); // NOLINT(readability-static-accessed-through-instance)
|
||||
text->writeColored(ROOMS_VALUE_X, LINE2_Y, ROOMS_TEXT, stringToColor("white"));
|
||||
|
||||
// Indicadores de trucos activos (fuente 8bithud)
|
||||
auto cheat_text = Resource::Cache::get()->getText("8bithud");
|
||||
if (Options::cheats.infinite_lives == Options::Cheat::State::ENABLED) {
|
||||
cheat_text->writeColored(CHEAT_INF_LIVES_X, CHEAT_INF_LIVES_Y, Locale::get()->get("scoreboard.cheat_infinite_lives"), data_->color); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
if (Options::cheats.invincible == Options::Cheat::State::ENABLED) {
|
||||
cheat_text->writeColored(CHEAT_INVINCIBLE_X, CHEAT_INVINCIBLE_Y, Locale::get()->get("scoreboard.cheat_invincibility"), data_->color); // NOLINT(readability-static-accessed-through-instance)
|
||||
}
|
||||
|
||||
// Deja el renderizador como estaba
|
||||
Screen::get()->setRendererSurface(previuos_renderer);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user