merge audita-nolint: 77→6 NOLINT (15 refactors + neteja obsolets)
This commit is contained in:
@@ -2,8 +2,10 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string> // Para allocator, operator+, char_traits, string
|
||||
#include <vector> // Para vector
|
||||
#include <algorithm> // Para std::ranges::find_if
|
||||
#include <initializer_list> // Para std::initializer_list
|
||||
#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
|
||||
@@ -158,65 +160,50 @@ namespace GlobalInputs {
|
||||
Notifier::get()->show({Locale::get()->get(Options::video.vertical_sync ? "ui.vsync_enabled" : "ui.vsync_disabled")});
|
||||
}
|
||||
|
||||
// F4 amb modificadors: Ctrl=toggle supersampling, Shift=next preset, sense modificador=toggle shader
|
||||
auto getShaderAction() -> InputAction {
|
||||
if (!Screen::get()->isHardwareAccelerated()) { return InputAction::NONE; }
|
||||
if (!Input::get()->checkAction(InputAction::TOGGLE_SHADER, Input::DO_NOT_ALLOW_REPEAT)) { return InputAction::NONE; }
|
||||
const SDL_Keymod MOD = SDL_GetModState();
|
||||
if ((MOD & SDL_KMOD_CTRL) != 0U) { return InputAction::TOGGLE_SUPERSAMPLING; }
|
||||
if (Options::video.shader.enabled && ((MOD & SDL_KMOD_SHIFT) != 0U)) { return InputAction::NEXT_SHADER_PRESET; }
|
||||
return InputAction::TOGGLE_SHADER;
|
||||
}
|
||||
|
||||
// F5 amb modificador Ctrl per a paleta anterior
|
||||
auto getPaletteAction() -> InputAction {
|
||||
if (!Input::get()->checkAction(InputAction::NEXT_PALETTE, Input::DO_NOT_ALLOW_REPEAT)) { return InputAction::NONE; }
|
||||
return ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) ? InputAction::PREVIOUS_PALETTE : InputAction::NEXT_PALETTE;
|
||||
}
|
||||
|
||||
// Comprova una llista d'accions 1:1 (sense modificadors); retorna la primera que dispare
|
||||
auto firstPressedFrom(std::initializer_list<InputAction> actions) -> InputAction {
|
||||
const auto* const IT = std::ranges::find_if(actions, [](const InputAction act) {
|
||||
return Input::get()->checkAction(act, Input::DO_NOT_ALLOW_REPEAT);
|
||||
});
|
||||
return (IT != actions.end()) ? *IT : InputAction::NONE;
|
||||
}
|
||||
|
||||
// SCREENSHOT requereix Ctrl mantingut a més de la tecla
|
||||
auto getScreenshotAction() -> InputAction {
|
||||
if (!Input::get()->checkAction(InputAction::SCREENSHOT, Input::DO_NOT_ALLOW_REPEAT)) { return InputAction::NONE; }
|
||||
return ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) ? InputAction::SCREENSHOT : InputAction::NONE;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
auto getPressedAction() -> InputAction {
|
||||
if (const InputAction ACT = firstPressedFrom({InputAction::EXIT, InputAction::ACCEPT, InputAction::TOGGLE_BORDER}); ACT != InputAction::NONE) { return ACT; }
|
||||
|
||||
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 (const InputAction ACT = firstPressedFrom({InputAction::TOGGLE_FULLSCREEN, InputAction::WINDOW_DEC_ZOOM, InputAction::WINDOW_INC_ZOOM}); ACT != InputAction::NONE) { return ACT; }
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (Input::get()->checkAction(InputAction::SCREENSHOT, Input::DO_NOT_ALLOW_REPEAT) &&
|
||||
((SDL_GetModState() & SDL_KMOD_CTRL) != 0U)) {
|
||||
return InputAction::SCREENSHOT;
|
||||
}
|
||||
return InputAction::NONE;
|
||||
|
||||
if (const InputAction ACT = getShaderAction(); ACT != InputAction::NONE) { return ACT; }
|
||||
if (const InputAction ACT = getPaletteAction(); ACT != InputAction::NONE) { return ACT; }
|
||||
|
||||
if (const InputAction ACT = firstPressedFrom({InputAction::NEXT_PALETTE_SORT, InputAction::TOGGLE_INTEGER_SCALE, InputAction::TOGGLE_VSYNC, InputAction::TOGGLE_INFO, InputAction::TOGGLE_CONSOLE}); ACT != InputAction::NONE) { return ACT; }
|
||||
|
||||
return getScreenshotAction();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace GIF {
|
||||
}
|
||||
|
||||
// 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)
|
||||
inline void initializeDictionary(std::vector<DictionaryEntry>& dictionary, int code_length, int& dictionary_ind) {
|
||||
int size = 1 << code_length;
|
||||
dictionary.resize(1 << (code_length + 1));
|
||||
for (dictionary_ind = 0; dictionary_ind < size; dictionary_ind++) {
|
||||
@@ -55,7 +55,7 @@ namespace GIF {
|
||||
}
|
||||
|
||||
// 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)
|
||||
inline void addDictionaryEntry(std::vector<DictionaryEntry>& dictionary, int& dictionary_ind, int& code_length, int prev, int code) {
|
||||
uint8_t first_byte;
|
||||
if (code == dictionary_ind) {
|
||||
first_byte = findFirstByte(dictionary, prev);
|
||||
|
||||
@@ -58,103 +58,123 @@ namespace {
|
||||
return count;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Construeix la matriu de cost NxM (ampliada a SZxSZ amb zeros) per a l'algoritme hongarès.
|
||||
auto buildCostMatrix(int n_rows, int m_cols, int sz, const Palette& palette, const Palette& reference) -> std::vector<int> {
|
||||
std::vector<int> cost(static_cast<size_t>(sz) * static_cast<size_t>(sz), 0);
|
||||
for (int i = 0; i < n_rows; ++i) {
|
||||
for (int j = 0; j < m_cols; ++j) {
|
||||
cost[(i * sz) + j] = rgbDistanceSq(palette[i], reference[j]);
|
||||
}
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
// Estat compartit entre les fases d'una iteració del Kuhn-Munkres
|
||||
struct HungarianStep {
|
||||
int j0;
|
||||
int delta;
|
||||
int j1;
|
||||
};
|
||||
|
||||
// Cerca la columna j1 que minimitza el cost reduït, actualitzant minv[] i way[].
|
||||
auto relaxColumns(int sz, const std::vector<int>& cost, int j0, int i0, const std::vector<bool>& used, const std::vector<int>& u, const std::vector<int>& v, std::vector<int>& minv, std::vector<int>& way) -> HungarianStep {
|
||||
constexpr int INF = INT_MAX / 2;
|
||||
HungarianStep step{.j0 = j0, .delta = INF, .j1 = 0};
|
||||
for (int j = 1; j <= sz; ++j) {
|
||||
if (used[j]) { continue; }
|
||||
const int CUR = cost[((i0 - 1) * sz) + (j - 1)] - u[i0] - v[j];
|
||||
if (CUR < minv[j]) {
|
||||
minv[j] = CUR;
|
||||
way[j] = j0;
|
||||
}
|
||||
if (minv[j] < step.delta) {
|
||||
step.delta = minv[j];
|
||||
step.j1 = j;
|
||||
}
|
||||
}
|
||||
return step;
|
||||
}
|
||||
|
||||
// Aplica el delta calculat a potencials (u, v) i a minv[].
|
||||
void applyDelta(int sz, int delta, const std::vector<bool>& used, const std::vector<int>& p, std::vector<int>& u, std::vector<int>& v, std::vector<int>& minv) {
|
||||
for (int j = 0; j <= sz; ++j) {
|
||||
if (used[j]) {
|
||||
u[p[j]] += delta;
|
||||
v[j] -= delta;
|
||||
} else {
|
||||
minv[j] -= delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Algoritme hongarès (Kuhn-Munkres) basat en potencials. Retorna p[],
|
||||
// on p[j] = fila assignada a la columna j (índexs 1-based; p[0] no s'usa).
|
||||
auto hungarianAssign(int sz, const std::vector<int>& cost) -> std::vector<int> {
|
||||
constexpr int INF = INT_MAX / 2;
|
||||
std::vector<int> u(sz + 1, 0); // Potencials de files
|
||||
std::vector<int> v(sz + 1, 0); // Potencials de columnes
|
||||
std::vector<int> p(sz + 1, 0); // p[j] = fila assignada a columna j
|
||||
std::vector<int> way(sz + 1, 0);
|
||||
|
||||
for (int i = 1; i <= sz; ++i) {
|
||||
p[0] = i;
|
||||
int j0 = 0;
|
||||
std::vector<int> minv(sz + 1, INF);
|
||||
std::vector<bool> used(sz + 1, false);
|
||||
|
||||
do {
|
||||
used[j0] = true;
|
||||
const auto STEP = relaxColumns(sz, cost, j0, p[j0], used, u, v, minv, way);
|
||||
applyDelta(sz, STEP.delta, used, p, u, v, minv);
|
||||
j0 = STEP.j1;
|
||||
} while (p[j0] != 0);
|
||||
|
||||
do {
|
||||
const int J1 = way[j0];
|
||||
p[j0] = p[J1];
|
||||
j0 = J1;
|
||||
} while (j0 != 0);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// Construeix la paleta resultant a partir de l'assignació p[]. Afegeix els colors
|
||||
// de palette sense parella en reference al final (cas N > M).
|
||||
auto buildPaletteFromAssignment(int n_rows, int m_cols, const std::vector<int>& p, const Palette& palette) -> Palette {
|
||||
Palette out{};
|
||||
out.fill(0);
|
||||
for (int j = 1; j <= m_cols && j <= n_rows; ++j) {
|
||||
const int ROW = p[j] - 1;
|
||||
if (ROW >= 0 && ROW < n_rows) { out[j - 1] = palette[ROW]; }
|
||||
}
|
||||
if (n_rows <= m_cols) { return out; }
|
||||
|
||||
std::vector<bool> used_rows(n_rows, false);
|
||||
for (int j = 1; j <= m_cols; ++j) {
|
||||
const int ROW = p[j] - 1;
|
||||
if (ROW >= 0 && ROW < n_rows) { used_rows[ROW] = true; }
|
||||
}
|
||||
int out_idx = m_cols;
|
||||
for (int i = 0; i < n_rows; ++i) {
|
||||
if (!used_rows[i]) { out[out_idx++] = palette[i]; }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Asignación óptima de colores mediante el algoritmo húngaro (Kuhn-Munkres).
|
||||
// Minimiza la distancia RGB total entre la paleta y la referencia.
|
||||
// O(N³) con N = número de colores activos — para N ≤ 256 es instantáneo.
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
||||
auto sortByOptimal(const Palette& palette, const Palette& reference) -> Palette {
|
||||
const auto N = static_cast<int>(countActiveColors(palette));
|
||||
const auto M = static_cast<int>(countActiveColors(reference));
|
||||
const int SZ = std::max(N, M);
|
||||
if (SZ == 0) { return palette; }
|
||||
|
||||
// Matriz de coste NxM (ampliada a SZxSZ con ceros para hacerla cuadrada)
|
||||
std::vector<int> cost(static_cast<size_t>(SZ) * static_cast<size_t>(SZ), 0);
|
||||
for (int i = 0; i < N; ++i) {
|
||||
for (int j = 0; j < M; ++j) {
|
||||
cost[(i * SZ) + j] = rgbDistanceSq(palette[i], reference[j]);
|
||||
}
|
||||
}
|
||||
|
||||
// Hungarian algorithm (Kuhn-Munkres) — versión basada en potenciales
|
||||
constexpr int INF = INT_MAX / 2;
|
||||
std::vector<int> u(SZ + 1, 0); // Potenciales de filas
|
||||
std::vector<int> v(SZ + 1, 0); // Potenciales de columnas
|
||||
std::vector<int> p(SZ + 1, 0); // p[j] = fila asignada a columna j
|
||||
std::vector<int> way(SZ + 1, 0);
|
||||
|
||||
for (int i = 1; i <= SZ; ++i) {
|
||||
p[0] = i;
|
||||
int j0 = 0;
|
||||
std::vector<int> minv(SZ + 1, INF);
|
||||
std::vector<bool> used(SZ + 1, false);
|
||||
|
||||
do {
|
||||
used[j0] = true;
|
||||
int i0 = p[j0];
|
||||
int delta = INF;
|
||||
int j1 = 0;
|
||||
|
||||
for (int j = 1; j <= SZ; ++j) {
|
||||
if (!used[j]) {
|
||||
int cur = cost[((i0 - 1) * SZ) + (j - 1)] - u[i0] - v[j];
|
||||
if (cur < minv[j]) {
|
||||
minv[j] = cur;
|
||||
way[j] = j0;
|
||||
}
|
||||
if (minv[j] < delta) {
|
||||
delta = minv[j];
|
||||
j1 = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 0; j <= SZ; ++j) {
|
||||
if (used[j]) {
|
||||
u[p[j]] += delta;
|
||||
v[j] -= delta;
|
||||
} else {
|
||||
minv[j] -= delta;
|
||||
}
|
||||
}
|
||||
|
||||
j0 = j1;
|
||||
} while (p[j0] != 0);
|
||||
|
||||
do {
|
||||
int j1 = way[j0];
|
||||
p[j0] = p[j1];
|
||||
j0 = j1;
|
||||
} while (j0 != 0);
|
||||
}
|
||||
|
||||
// Construir la paleta resultante: assignment[j] = fila asignada a columna j
|
||||
// Queremos result[j] = palette[fila asignada a j]
|
||||
Palette out{};
|
||||
out.fill(0);
|
||||
for (int j = 1; j <= M && j <= N; ++j) {
|
||||
int row = p[j] - 1; // Índice 0-based en palette
|
||||
if (row >= 0 && row < N) {
|
||||
out[j - 1] = palette[row];
|
||||
}
|
||||
}
|
||||
|
||||
// Colores extra de palette que no tienen pareja en reference (N > M)
|
||||
if (N > M) {
|
||||
std::vector<bool> used_rows(N, false);
|
||||
for (int j = 1; j <= M; ++j) {
|
||||
int row = p[j] - 1;
|
||||
if (row >= 0 && row < N) { used_rows[row] = true; }
|
||||
}
|
||||
int out_idx = M;
|
||||
for (int i = 0; i < N; ++i) {
|
||||
if (!used_rows[i]) {
|
||||
out[out_idx++] = palette[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
const auto COST = buildCostMatrix(N, M, SZ, palette, reference);
|
||||
const auto P = hungarianAssign(SZ, COST);
|
||||
return buildPaletteFromAssignment(N, M, P, palette);
|
||||
}
|
||||
|
||||
// Asignación greedy de colores a la paleta de referencia.
|
||||
|
||||
@@ -66,7 +66,7 @@ PixelReveal::PixelReveal(int width, int height, float pixels_per_second, float s
|
||||
}
|
||||
|
||||
// Actualiza el estado del revelado
|
||||
void PixelReveal::update(float time_active) { // NOLINT(readability-make-member-function-const)
|
||||
void PixelReveal::update(float time_active) {
|
||||
// En modo normal revela (pone transparente); en modo inverso cubre (pone negro)
|
||||
const auto PIXEL_COLOR = reverse_ ? 0 : 255;
|
||||
|
||||
|
||||
@@ -808,7 +808,7 @@ auto Screen::initSDLVideo() -> bool {
|
||||
|
||||
// Registra los callbacks nativos de Emscripten que restauran el canvas cuando
|
||||
// SDL3 no emite los events equivalentes. Fuera de Emscripten es un no-op.
|
||||
void Screen::registerEmscriptenEventCallbacks() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
void Screen::registerEmscriptenEventCallbacks() {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// NO registramos resize callback. En móvil, el scroll hace que el navegador
|
||||
// oculte/muestre la barra de URL, disparando un resize del DOM por cada scroll,
|
||||
|
||||
@@ -162,25 +162,30 @@ namespace Rendering {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// createPipeline
|
||||
// createPostfxVertexShader — fullscreen-triangle vertex compartit per tots els pipelines
|
||||
// ---------------------------------------------------------------------------
|
||||
auto SDL3GPUShader::createPipeline() -> bool { // NOLINT(readability-function-cognitive-complexity)
|
||||
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||
|
||||
// ---- PostFX pipeline (scene/scaled → swapchain) ----
|
||||
auto SDL3GPUShader::createPostfxVertexShader() -> SDL_GPUShader* {
|
||||
#ifdef __APPLE__
|
||||
SDL_GPUShader* vert = createShaderMSL(device_, Rendering::Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* frag = createShaderMSL(device_, Rendering::Msl::kPostfxFrag, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
return createShaderMSL(device_, Rendering::Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
#else
|
||||
SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* frag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
return createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
if ((vert == nullptr) || (frag == nullptr)) {
|
||||
SDL_Log("SDL3GPUShader: failed to compile PostFX shaders");
|
||||
if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); }
|
||||
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
|
||||
return false;
|
||||
// ---------------------------------------------------------------------------
|
||||
// createPostfxLikePipeline — empaqueta vert(postfx) + frag dado + target en un pipeline.
|
||||
// Pren ownership de `frag` (el libera abans de retornar).
|
||||
// ---------------------------------------------------------------------------
|
||||
auto SDL3GPUShader::createPostfxLikePipeline(SDL_GPUShader* frag, SDL_GPUTextureFormat format, const char* debug_name) -> SDL_GPUGraphicsPipeline* {
|
||||
if (frag == nullptr) {
|
||||
SDL_Log("SDL3GPUShader: %s frag shader is null", debug_name);
|
||||
return nullptr;
|
||||
}
|
||||
SDL_GPUShader* vert = createPostfxVertexShader();
|
||||
if (vert == nullptr) {
|
||||
SDL_Log("SDL3GPUShader: %s vert shader creation failed", debug_name);
|
||||
SDL_ReleaseGPUShader(device_, frag);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SDL_GPUColorTargetBlendState no_blend = {};
|
||||
@@ -188,30 +193,44 @@ namespace Rendering {
|
||||
no_blend.enable_color_write_mask = false;
|
||||
|
||||
SDL_GPUColorTargetDescription color_target = {};
|
||||
color_target.format = SWAPCHAIN_FMT;
|
||||
color_target.format = format;
|
||||
color_target.blend_state = no_blend;
|
||||
|
||||
SDL_GPUVertexInputState no_input = {};
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo pipe_info = {};
|
||||
pipe_info.vertex_shader = vert;
|
||||
pipe_info.fragment_shader = frag;
|
||||
pipe_info.vertex_input_state = no_input;
|
||||
pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
pipe_info.target_info.num_color_targets = 1;
|
||||
pipe_info.target_info.color_target_descriptions = &color_target;
|
||||
SDL_GPUGraphicsPipelineCreateInfo info = {};
|
||||
info.vertex_shader = vert;
|
||||
info.fragment_shader = frag;
|
||||
info.vertex_input_state = no_input;
|
||||
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
info.target_info.num_color_targets = 1;
|
||||
info.target_info.color_target_descriptions = &color_target;
|
||||
|
||||
pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &pipe_info);
|
||||
SDL_GPUGraphicsPipeline* pipeline = SDL_CreateGPUGraphicsPipeline(device_, &info);
|
||||
|
||||
SDL_ReleaseGPUShader(device_, vert);
|
||||
SDL_ReleaseGPUShader(device_, frag);
|
||||
|
||||
if (pipeline_ == nullptr) {
|
||||
SDL_Log("SDL3GPUShader: PostFX pipeline creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
if (pipeline == nullptr) {
|
||||
SDL_Log("SDL3GPUShader: %s pipeline creation failed: %s", debug_name, SDL_GetError());
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
return true;
|
||||
// ---------------------------------------------------------------------------
|
||||
// createPipeline — pipeline únic PostFX → swapchain
|
||||
// ---------------------------------------------------------------------------
|
||||
auto SDL3GPUShader::createPipeline() -> bool {
|
||||
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||
|
||||
#ifdef __APPLE__
|
||||
SDL_GPUShader* postfx_frag = createShaderMSL(device_, Rendering::Msl::kPostfxFrag, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#else
|
||||
SDL_GPUShader* postfx_frag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#endif
|
||||
|
||||
pipeline_ = createPostfxLikePipeline(postfx_frag, SWAPCHAIN_FMT, "PostFX");
|
||||
return pipeline_ != nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -222,51 +241,13 @@ namespace Rendering {
|
||||
// ---------------------------------------------------------------------------
|
||||
auto SDL3GPUShader::createCrtPiPipeline() -> bool {
|
||||
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||
|
||||
#ifdef __APPLE__
|
||||
SDL_GPUShader* vert = createShaderMSL(device_, Rendering::Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* frag = createShaderMSL(device_, Rendering::Msl::kCrtpiFrag, "crtpi_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#else
|
||||
SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#endif
|
||||
|
||||
if ((vert == nullptr) || (frag == nullptr)) {
|
||||
SDL_Log("SDL3GPUShader: failed to compile CrtPi shaders");
|
||||
if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); }
|
||||
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GPUColorTargetBlendState no_blend = {};
|
||||
no_blend.enable_blend = false;
|
||||
no_blend.enable_color_write_mask = false;
|
||||
|
||||
SDL_GPUColorTargetDescription color_target = {};
|
||||
color_target.format = SWAPCHAIN_FMT;
|
||||
color_target.blend_state = no_blend;
|
||||
|
||||
SDL_GPUVertexInputState no_input = {};
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo pipe_info = {};
|
||||
pipe_info.vertex_shader = vert;
|
||||
pipe_info.fragment_shader = frag;
|
||||
pipe_info.vertex_input_state = no_input;
|
||||
pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
pipe_info.target_info.num_color_targets = 1;
|
||||
pipe_info.target_info.color_target_descriptions = &color_target;
|
||||
|
||||
crtpi_pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &pipe_info);
|
||||
|
||||
SDL_ReleaseGPUShader(device_, vert);
|
||||
SDL_ReleaseGPUShader(device_, frag);
|
||||
|
||||
if (crtpi_pipeline_ == nullptr) {
|
||||
SDL_Log("SDL3GPUShader: CrtPi pipeline creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
crtpi_pipeline_ = createPostfxLikePipeline(frag, SWAPCHAIN_FMT, "CrtPi");
|
||||
return crtpi_pipeline_ != nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -124,8 +124,10 @@ namespace Rendering {
|
||||
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 createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
||||
auto createPostfxVertexShader() -> SDL_GPUShader*; // Vertex shader fullscreen-triangle compartit (MSL/SPIRV)
|
||||
auto createPostfxLikePipeline(SDL_GPUShader* frag, SDL_GPUTextureFormat format, const char* debug_name) -> SDL_GPUGraphicsPipeline*; // Empaqueta vert + frag + target en un pipeline
|
||||
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
||||
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
|
||||
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
||||
|
||||
|
||||
@@ -148,14 +148,14 @@ void Surface::setColor(int index, Uint32 color) {
|
||||
}
|
||||
|
||||
// Rellena la superficie con un color
|
||||
void Surface::clear(Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
void Surface::clear(Uint8 color) {
|
||||
const size_t TOTAL_PIXELS = static_cast<size_t>(surface_data_->width) * static_cast<size_t>(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)
|
||||
void Surface::putPixel(int x, int y, Uint8 color) {
|
||||
if (x < 0 || y < 0 || x >= surface_data_->width || y >= surface_data_->height) {
|
||||
return; // Coordenadas fuera de rango
|
||||
}
|
||||
@@ -168,7 +168,7 @@ void Surface::putPixel(int x, int y, Uint8 color) { // NOLINT(readability-conve
|
||||
auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * surface_data_->width)]; }
|
||||
|
||||
// Dibuja un rectangulo relleno
|
||||
void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
void Surface::fillRect(const SDL_FRect* rect, Uint8 color) {
|
||||
// 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);
|
||||
@@ -185,7 +185,7 @@ void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readabil
|
||||
}
|
||||
|
||||
// Dibuja el borde de un rectangulo
|
||||
void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) {
|
||||
// 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);
|
||||
@@ -212,7 +212,7 @@ void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(re
|
||||
}
|
||||
|
||||
// Dibuja una linea (Bresenham en enteros)
|
||||
void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) {
|
||||
int ix1 = static_cast<int>(std::lround(x1));
|
||||
int iy1 = static_cast<int>(std::lround(y1));
|
||||
const int IX2 = static_cast<int>(std::lround(x2));
|
||||
@@ -247,7 +247,7 @@ void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { /
|
||||
}
|
||||
}
|
||||
|
||||
void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { // NOLINT(readability-make-member-function-const)
|
||||
void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) {
|
||||
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
|
||||
|
||||
// Aplicar render offset (usado por transiciones entre pantallas)
|
||||
@@ -533,7 +533,7 @@ void Surface::toARGBBuffer(Uint32* buffer) const {
|
||||
}
|
||||
|
||||
// Vuelca la superficie a una textura
|
||||
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) {
|
||||
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||
throw std::runtime_error("Renderer or texture is null.");
|
||||
}
|
||||
@@ -575,7 +575,7 @@ void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { //
|
||||
}
|
||||
|
||||
// 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)
|
||||
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect) {
|
||||
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
|
||||
throw std::runtime_error("Renderer or texture is null.");
|
||||
}
|
||||
@@ -624,7 +624,7 @@ void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FR
|
||||
}
|
||||
|
||||
// Realiza un efecto de fundido en la paleta principal
|
||||
auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto Surface::fadePalette() -> bool {
|
||||
static constexpr int PALETTE_SIZE = 19;
|
||||
static_assert(std::tuple_size_v<Palette> >= PALETTE_SIZE, "Palette size is insufficient for fadePalette operation.");
|
||||
|
||||
@@ -641,7 +641,7 @@ auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-func
|
||||
}
|
||||
|
||||
// Realiza un efecto de fundido en la paleta secundaria
|
||||
auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto Surface::fadeSubPalette(Uint32 delay) -> bool {
|
||||
// Variable estática para almacenar el último tick
|
||||
static Uint32 last_tick_ = 0;
|
||||
|
||||
@@ -672,4 +672,4 @@ auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-conv
|
||||
}
|
||||
|
||||
// Restaura la sub paleta a su estado original
|
||||
void Surface::resetSubPalette() { initializeSubPalette(sub_palette_); } // NOLINT(readability-convert-member-functions-to-static)
|
||||
void Surface::resetSubPalette() { initializeSubPalette(sub_palette_); }
|
||||
|
||||
@@ -167,7 +167,7 @@ void Text::write(int x, int y, const std::string& text, int kerning, int lenght)
|
||||
}
|
||||
|
||||
// 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 Text::writeToSurface(const std::string& text, int zoom, int kerning) -> std::shared_ptr<Surface> {
|
||||
auto width = length(text, kerning) * zoom;
|
||||
auto height = box_height_ * zoom;
|
||||
auto surface = std::make_shared<Surface>(width, height);
|
||||
@@ -181,7 +181,7 @@ auto Text::writeToSurface(const std::string& text, int zoom, int kerning) -> std
|
||||
}
|
||||
|
||||
// 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 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> {
|
||||
auto width = Text::length(text, kerning) + shadow_distance;
|
||||
auto height = box_height_ + shadow_distance;
|
||||
auto surface = std::make_shared<Surface>(width, height);
|
||||
|
||||
@@ -244,14 +244,13 @@ namespace Resource {
|
||||
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) {
|
||||
// Helper para registrar errores de carga con formato consistente.
|
||||
// El rethrow es responsabilitat del catch que crida la funció.
|
||||
void Cache::logLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e) {
|
||||
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";
|
||||
// cppcheck-suppress rethrowNoCurrentException -- helper [[noreturn]] invocado desde dentro de un catch; cppcheck no puede ver que el rethrow es válido a través de la llamada.
|
||||
throw;
|
||||
}
|
||||
|
||||
// Carga los sonidos
|
||||
@@ -284,7 +283,8 @@ namespace Resource {
|
||||
printWithDots("Sound : ", name, "[ LOADED ]");
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("SOUND", l, e);
|
||||
logLoadError("SOUND", l, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,7 +319,8 @@ namespace Resource {
|
||||
printWithDots("Music : ", name, "[ LOADED ]");
|
||||
updateLoadingProgress(1);
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("MUSIC", l, e);
|
||||
logLoadError("MUSIC", l, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,7 +338,8 @@ namespace Resource {
|
||||
surfaces_.back().surface->setTransparentColor(0);
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("BITMAP", l, e);
|
||||
logLoadError("BITMAP", l, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +362,8 @@ namespace Resource {
|
||||
palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)});
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("PALETTE", l, e);
|
||||
logLoadError("PALETTE", l, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,7 +380,8 @@ namespace Resource {
|
||||
text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)});
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("FONT", l, e);
|
||||
logLoadError("FONT", l, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -403,7 +407,8 @@ namespace Resource {
|
||||
printWithDots("Animation : ", name, "[ LOADED ]");
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("ANIMATION", l, e);
|
||||
logLoadError("ANIMATION", l, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,7 +426,8 @@ namespace Resource {
|
||||
printWithDots("Room : ", name, "[ LOADED ]");
|
||||
updateLoadingProgress();
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("ROOM", l, e);
|
||||
logLoadError("ROOM", l, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -601,7 +607,8 @@ namespace Resource {
|
||||
it->sound = sound;
|
||||
std::cout << "[lazy] Sound loaded: " << name << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("SOUND", path, e);
|
||||
logLoadError("SOUND", path, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,7 +625,8 @@ namespace Resource {
|
||||
it->music = music;
|
||||
std::cout << "[lazy] Music loaded: " << name << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("MUSIC", path, e);
|
||||
logLoadError("MUSIC", path, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,7 +646,8 @@ namespace Resource {
|
||||
}
|
||||
std::cout << "[lazy] Surface loaded: " << name << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("BITMAP", path, e);
|
||||
logLoadError("BITMAP", path, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,7 +660,8 @@ namespace Resource {
|
||||
it->loaded = true;
|
||||
std::cout << "[lazy] Palette loaded: " << name << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("PALETTE", path, e);
|
||||
logLoadError("PALETTE", path, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,7 +673,8 @@ namespace Resource {
|
||||
it->text_file = Text::loadTextFile(path);
|
||||
std::cout << "[lazy] TextFile loaded: " << name << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("FONT", path, e);
|
||||
logLoadError("FONT", path, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -677,7 +688,8 @@ namespace Resource {
|
||||
it->yaml_data = bytes;
|
||||
std::cout << "[lazy] Animation loaded: " << name << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("ANIMATION", path, e);
|
||||
logLoadError("ANIMATION", path, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,7 +701,8 @@ namespace Resource {
|
||||
it->room = std::make_shared<Room::Data>(Room::loadYAML(path));
|
||||
std::cout << "[lazy] Room loaded: " << name << '\n';
|
||||
} catch (const std::exception& e) {
|
||||
throwLoadError("ROOM", path, e);
|
||||
logLoadError("ROOM", path, e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace Resource {
|
||||
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);
|
||||
static void logLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e);
|
||||
|
||||
// Constructor y destructor
|
||||
explicit Cache(LoadingMode mode);
|
||||
|
||||
@@ -16,6 +16,71 @@
|
||||
|
||||
namespace Resource {
|
||||
|
||||
namespace {
|
||||
// Un item del format modern: pot ser string (path) o mapping ({path, required?, absolute?})
|
||||
void parseAssetItem(List& list, const fkyaml::node& item, List::Type type, const std::string& prefix, const std::string& system_folder, const std::string& category, const std::string& type_str) {
|
||||
try {
|
||||
if (item.is_string()) {
|
||||
auto path = List::replaceVariables(item.get_value<std::string>(), prefix, system_folder);
|
||||
list.add(path, type, true, false);
|
||||
return;
|
||||
}
|
||||
if (item.is_mapping() && item.contains("path")) {
|
||||
auto path = List::replaceVariables(item["path"].get_value<std::string>(), prefix, system_folder);
|
||||
const bool REQUIRED = !item.contains("required") || item["required"].get_value<bool>();
|
||||
const bool ABSOLUTE = item.contains("absolute") && item["absolute"].get_value<bool>();
|
||||
list.add(path, type, REQUIRED, ABSOLUTE);
|
||||
return;
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
// (TIPO: [items...]) del format modern. Itera els items i delega a parseAssetItem.
|
||||
void parseModernType(List& list, const fkyaml::node& items_node, List::Type type, const std::string& type_str, const std::string& category, const std::string& prefix, const std::string& system_folder) {
|
||||
if (!items_node.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());
|
||||
return;
|
||||
}
|
||||
for (const auto& item : items_node) {
|
||||
parseAssetItem(list, item, type, prefix, system_folder, category, type_str);
|
||||
}
|
||||
}
|
||||
|
||||
// {type, path, required?, absolute?} del format antic
|
||||
void parseLegacyAsset(List& list, const fkyaml::node& asset, const std::string& category, const std::string& prefix, const std::string& system_folder) {
|
||||
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());
|
||||
return;
|
||||
}
|
||||
auto type_str = asset["type"].get_value<std::string>();
|
||||
auto path = asset["path"].get_value<std::string>();
|
||||
const bool REQUIRED = !asset.contains("required") || asset["required"].get_value<bool>();
|
||||
const bool ABSOLUTE = asset.contains("absolute") && asset["absolute"].get_value<bool>();
|
||||
path = List::replaceVariables(path, prefix, system_folder);
|
||||
list.add(path, List::parseAssetType(type_str), 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());
|
||||
}
|
||||
}
|
||||
|
||||
// Categoria amb format modern (TIPO → [items]). Itera els tipus.
|
||||
void parseModernCategory(List& list, const fkyaml::node& category_assets, const std::string& category, const std::string& prefix, const std::string& system_folder) {
|
||||
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>();
|
||||
const List::Type TYPE = List::parseAssetType(type_str);
|
||||
parseModernType(list, type_it.value(), TYPE, type_str, category, prefix, system_folder);
|
||||
} catch (const std::exception& e) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error parsing type in category '%s': %s", category.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Singleton
|
||||
List* List::instance = nullptr;
|
||||
|
||||
@@ -160,17 +225,13 @@ namespace Resource {
|
||||
}
|
||||
|
||||
// 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)
|
||||
void List::loadFromString(const std::string& config_content, const std::string& prefix, const std::string& system_folder) {
|
||||
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.)
|
||||
@@ -179,105 +240,19 @@ namespace Resource {
|
||||
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());
|
||||
}
|
||||
}
|
||||
parseModernCategory(*this, category_assets, category, prefix, system_folder);
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
for (const auto& asset : category_assets) { parseLegacyAsset(*this, asset, category, prefix, system_folder); }
|
||||
} else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Warning: Category '%s' has invalid format, skipping",
|
||||
category.c_str());
|
||||
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());
|
||||
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());
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error loading assets: %s", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,10 @@ namespace Resource {
|
||||
[[nodiscard]] auto getListByType(Type type) const -> std::vector<std::string>;
|
||||
[[nodiscard]] auto exists(const std::string& filename) const -> bool; // Verifica si un asset existe
|
||||
|
||||
// Helpers públics per al parseig YAML (usats des del namespace anònim al .cpp)
|
||||
[[nodiscard]] static auto parseAssetType(const std::string& type_str) -> Type;
|
||||
[[nodiscard]] static auto replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string;
|
||||
|
||||
private:
|
||||
// --- Estructuras privadas ---
|
||||
struct Item {
|
||||
@@ -62,11 +66,9 @@ namespace Resource {
|
||||
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
|
||||
[[nodiscard]] static auto getTypeName(Type type) -> std::string; // Obtiene el nombre del tipo
|
||||
void addToMap(const std::string& file_path, Type type, bool required, bool absolute); // Añade archivo al mapa
|
||||
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
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Resource {
|
||||
}
|
||||
|
||||
// Load a resource
|
||||
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> { // NOLINT(readability-make-member-function-const)
|
||||
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
|
||||
if (!initialized_) {
|
||||
std::cerr << "Loader: Not initialized\n";
|
||||
return {};
|
||||
@@ -81,7 +81,7 @@ namespace Resource {
|
||||
}
|
||||
|
||||
// Check if a resource exists
|
||||
auto Loader::resourceExists(const std::string& filename) -> bool { // NOLINT(readability-make-member-function-const)
|
||||
auto Loader::resourceExists(const std::string& filename) -> bool {
|
||||
if (!initialized_) {
|
||||
return false;
|
||||
}
|
||||
@@ -158,7 +158,7 @@ namespace Resource {
|
||||
|
||||
if (checksum == 0) {
|
||||
std::cerr << "Loader: Pack checksum is zero (invalid)\n";
|
||||
return false; // NOLINT(readability-simplify-boolean-expr)
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "Loader: Pack checksum: 0x" << std::hex << checksum << std::dec
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Resource {
|
||||
}
|
||||
|
||||
// XOR encryption (symmetric - same function for encrypt/decrypt)
|
||||
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) { // NOLINT(readability-identifier-naming)
|
||||
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||
if (key.empty()) {
|
||||
return;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ namespace Resource {
|
||||
}
|
||||
}
|
||||
|
||||
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) { // NOLINT(readability-identifier-naming)
|
||||
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||
// XOR is symmetric
|
||||
encryptData(data, key);
|
||||
}
|
||||
@@ -79,7 +79,7 @@ namespace Resource {
|
||||
// Add all files from a directory recursively
|
||||
auto Pack::addDirectory(const std::string& dir_path,
|
||||
const std::string& base_path) -> bool {
|
||||
namespace fs = std::filesystem; // NOLINT(readability-identifier-naming)
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
|
||||
std::cerr << "ResourcePack: Directory not found: " << dir_path << '\n';
|
||||
|
||||
@@ -32,7 +32,7 @@ auto Debug::get() -> Debug* {
|
||||
}
|
||||
|
||||
// Dibuja en pantalla
|
||||
void Debug::render() { // NOLINT(readability-make-member-function-const)
|
||||
void Debug::render() {
|
||||
auto text = Resource::Cache::get()->getText("aseprite");
|
||||
int y = y_;
|
||||
int w = 0;
|
||||
|
||||
@@ -433,7 +433,7 @@ void MapEditor::render() {
|
||||
}
|
||||
|
||||
// Maneja eventos del editor
|
||||
void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-function-cognitive-complexity)
|
||||
void MapEditor::handleEvent(const SDL_Event& event) {
|
||||
// Si el tile picker está abierto, los eventos van a él.
|
||||
// Excepción: la T lo cierra como toggle (sin tocar el brush).
|
||||
const auto* kc = KeyConfig::get();
|
||||
@@ -708,7 +708,6 @@ void MapEditor::handleMouseUp() {
|
||||
}
|
||||
|
||||
// Commit de un drag de entidad (initial, bound1, bound2) para cualquier EntityType.
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- switch sobre EntityType con una rama por tipo; refactor a visitor requiere cambio de diseño.
|
||||
auto MapEditor::commitEntityDrag() -> bool {
|
||||
const int IDX = drag_.index;
|
||||
const int SNAP_X = static_cast<int>(drag_.snap_x);
|
||||
@@ -816,7 +815,6 @@ auto MapEditor::commitEntityDrag() -> bool {
|
||||
}
|
||||
|
||||
// Mueve visualmente la entidad arrastrada a la posición snapped.
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- switch sobre EntityType con cases paralelos para cada tipo.
|
||||
void MapEditor::moveEntityVisual() {
|
||||
switch (drag_.target) {
|
||||
case DragTarget::ENTITY_INITIAL:
|
||||
@@ -958,7 +956,6 @@ void MapEditor::renderSelectionHighlight() {
|
||||
}
|
||||
|
||||
// Estampa el patrón del brush en la posición indicada (anclaje top-left).
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- nested loops + casos TRANSPARENT/ERASE/tile normal y ramas collision vs normal.
|
||||
void MapEditor::stampBrushAt(int tile_x, int tile_y) {
|
||||
if (brush_.isEmpty()) { return; }
|
||||
for (int dy = 0; dy < brush_.height; ++dy) {
|
||||
@@ -1233,7 +1230,6 @@ auto MapEditor::entityLabel(EntityType type) -> const char* {
|
||||
}
|
||||
|
||||
// Dibuja marcadores de boundaries y líneas de ruta para enemigos y plataformas.
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- switch sobre EntityType con ramas de enemigo patrullante vs plataforma con waypoints.
|
||||
void MapEditor::renderEntityBoundaries() {
|
||||
auto game_surface = Screen::get()->getRendererSurface();
|
||||
if (!game_surface) { return; }
|
||||
@@ -1374,7 +1370,7 @@ void MapEditor::updateMousePosition() {
|
||||
}
|
||||
|
||||
// Actualiza la información de la barra de estado
|
||||
void MapEditor::updateStatusBarInfo() { // NOLINT(readability-function-cognitive-complexity)
|
||||
void MapEditor::updateStatusBarInfo() {
|
||||
if (!statusbar_) { return; }
|
||||
|
||||
statusbar_->setMouseTile(mouse_tile_x_, mouse_tile_y_);
|
||||
@@ -1563,7 +1559,7 @@ auto MapEditor::getAnimationCompletions() const -> std::vector<std::string> {
|
||||
}
|
||||
|
||||
// Modifica una propiedad del enemigo seleccionado
|
||||
auto MapEditor::setEnemyProperty(const std::string& property, const std::string& value) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
auto MapEditor::setEnemyProperty(const std::string& property, const std::string& value) -> std::string {
|
||||
if (!active_) { return "Editor not active"; }
|
||||
if (!hasSelectedEnemy()) { return "No enemy selected"; }
|
||||
|
||||
@@ -1730,7 +1726,7 @@ auto MapEditor::duplicateEnemy() -> std::string {
|
||||
}
|
||||
|
||||
// Modifica una propiedad de la habitación
|
||||
auto MapEditor::setRoomProperty(const std::string& property, const std::string& value) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
auto MapEditor::setRoomProperty(const std::string& property, const std::string& value) -> std::string {
|
||||
if (!active_) { return "Editor not active"; }
|
||||
|
||||
std::string val = toLower(value);
|
||||
@@ -1921,7 +1917,7 @@ auto MapEditor::setRoomProperty(const std::string& property, const std::string&
|
||||
}
|
||||
|
||||
// Crea una nueva habitación
|
||||
auto MapEditor::createNewRoom(const std::string& direction) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
auto MapEditor::createNewRoom(const std::string& direction) -> std::string {
|
||||
if (!active_) { return "Editor not active"; }
|
||||
|
||||
// Validar dirección si se proporcionó
|
||||
@@ -2056,7 +2052,7 @@ auto MapEditor::createNewRoom(const std::string& direction) -> std::string { //
|
||||
}
|
||||
|
||||
// Elimina la habitación actual
|
||||
auto MapEditor::deleteRoom() -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
auto MapEditor::deleteRoom() -> std::string {
|
||||
if (!active_) { return "Editor not active"; }
|
||||
|
||||
std::string deleted_name = room_path_;
|
||||
|
||||
@@ -149,7 +149,7 @@ class MapEditor {
|
||||
[[nodiscard]] auto getSelectionType() const -> EntityType { return selection_.type; }
|
||||
|
||||
private:
|
||||
static MapEditor* instance_; // NOLINT(readability-identifier-naming) [SINGLETON] Objeto privado
|
||||
static MapEditor* instance_;
|
||||
|
||||
MapEditor(); // Constructor
|
||||
~MapEditor(); // Destructor
|
||||
|
||||
@@ -38,7 +38,7 @@ void PathEnemy::resetToInitialPosition(const Data& data) {
|
||||
#endif
|
||||
|
||||
// Comprueba si ha llegado al limite del recorrido para darse media vuelta
|
||||
void PathEnemy::checkPath() { // NOLINT(readability-make-member-function-const)
|
||||
void PathEnemy::checkPath() {
|
||||
if (sprite_->getPosX() > x2_ || sprite_->getPosX() < x1_) {
|
||||
// Recoloca
|
||||
if (sprite_->getPosX() > x2_) {
|
||||
|
||||
@@ -632,7 +632,7 @@ void Player::placeSprite() {
|
||||
sprite_->setPos(x_, y_);
|
||||
}
|
||||
|
||||
void Player::animate(float delta_time) { // NOLINT(readability-make-member-function-const)
|
||||
void Player::animate(float delta_time) {
|
||||
if (state_ == State::ON_AIR) {
|
||||
turning_ = false;
|
||||
const bool NEAR_PEAK = vy_ > JUMP_VELOCITY * 0.5F && vy_ < -JUMP_VELOCITY * 0.5F;
|
||||
|
||||
@@ -17,7 +17,7 @@ DoorManager::DoorManager(std::string room_id, SolidActorManager* solid_actors)
|
||||
// Añade una puerta y la registra en el SolidActorManager. El bit
|
||||
// BLOCKS_PLAYER del propio Door determina si bloquea al Player (se setea en
|
||||
// el constructor si la puerta no arranca ya abierta desde el DoorTracker).
|
||||
void DoorManager::addDoor(std::shared_ptr<Door> door) { // NOLINT(readability-identifier-naming)
|
||||
void DoorManager::addDoor(std::shared_ptr<Door> door) {
|
||||
solid_actors_->registerActor(door.get());
|
||||
doors_.push_back(std::move(door));
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#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)
|
||||
void EnemyManager::addEnemy(std::shared_ptr<Enemy> enemy) {
|
||||
enemies_.push_back(std::move(enemy));
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ ItemManager::ItemManager(std::string room_id, std::shared_ptr<Scoreboard::Data>
|
||||
}
|
||||
|
||||
// Añade un item a la colección
|
||||
void ItemManager::addItem(std::shared_ptr<Item> item) { // NOLINT(readability-identifier-naming)
|
||||
void ItemManager::addItem(std::shared_ptr<Item> item) {
|
||||
items_.push_back(std::move(item));
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ KeyManager::KeyManager(std::string room_id)
|
||||
}
|
||||
|
||||
// Añade una llave a la colección
|
||||
void KeyManager::addKey(std::shared_ptr<Key> key) { // NOLINT(readability-identifier-naming)
|
||||
void KeyManager::addKey(std::shared_ptr<Key> key) {
|
||||
keys_.push_back(std::move(key));
|
||||
}
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ void Room::setBgColor(Uint8 bg_color) {
|
||||
#endif
|
||||
|
||||
// Actualiza las variables y objetos de la habitación
|
||||
void Room::update(float delta_time) { // NOLINT(readability-make-member-function-const)
|
||||
void Room::update(float delta_time) {
|
||||
if (is_paused_) {
|
||||
// Si está en modo pausa no se actualiza nada
|
||||
return;
|
||||
|
||||
@@ -61,7 +61,8 @@ class Room {
|
||||
|
||||
// Constructor y destructor
|
||||
Room(const std::string& room_path, std::shared_ptr<Scoreboard::Data> data);
|
||||
~Room(); // NOLINT(modernize-use-equals-default, performance-trivially-destructible)
|
||||
// NOLINTNEXTLINE(modernize-use-equals-default,performance-trivially-destructible) -- destructor definit al .cpp perquè la classe té unique_ptr a tipus forward-declared; no es pot fer = default ni eliminar a l'header sense incloure tots els headers transitivament.
|
||||
~Room();
|
||||
|
||||
// --- Funciones ---
|
||||
[[nodiscard]] auto getNumber() const -> const std::string& { return number_; }
|
||||
|
||||
@@ -548,31 +548,17 @@ auto RoomFormat::roomConnectionToYAML(const std::string& connection) -> std::str
|
||||
return connection;
|
||||
}
|
||||
|
||||
auto RoomFormat::buildContent(const Room::Data& room_data) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
std::ostringstream out;
|
||||
|
||||
// --- Sección room ---
|
||||
void RoomFormat::writeRoomSection(std::ostringstream& out, const Room::Data& room_data) {
|
||||
out << "room:\n";
|
||||
|
||||
// zone es siempre obligatoria
|
||||
out << " zone: " << room_data.zone << "\n";
|
||||
|
||||
// tileSetFile solo si es override explícito del valor heredado de la zona
|
||||
if (room_data.tile_set_overridden) {
|
||||
out << " tileSetFile: " << room_data.tile_set_file << "\n";
|
||||
}
|
||||
// tileSetFile/music/bgColor solo si son override explícito del valor heredado de la zona
|
||||
if (room_data.tile_set_overridden) { out << " tileSetFile: " << room_data.tile_set_file << "\n"; }
|
||||
if (room_data.music_overridden) { out << " music: " << room_data.music << "\n"; }
|
||||
if (room_data.bg_color_overridden) { out << " bgColor: " << static_cast<int>(room_data.bg_color) << "\n"; }
|
||||
|
||||
// music solo si es override explícito del valor heredado de la zona
|
||||
if (room_data.music_overridden) {
|
||||
out << " music: " << room_data.music << "\n";
|
||||
}
|
||||
|
||||
// bgColor solo si es override explícito del valor heredado de la zona
|
||||
if (room_data.bg_color_overridden) {
|
||||
out << " bgColor: " << static_cast<int>(room_data.bg_color) << "\n";
|
||||
}
|
||||
|
||||
// Conexiones
|
||||
out << "\n";
|
||||
out << " # Conexiones de la habitación (null = sin conexión)\n";
|
||||
out << " connections:\n";
|
||||
@@ -580,155 +566,137 @@ auto RoomFormat::buildContent(const Room::Data& room_data) -> std::string { //
|
||||
out << " down: " << roomConnectionToYAML(room_data.lower_room) << "\n";
|
||||
out << " left: " << roomConnectionToYAML(room_data.left_room) << "\n";
|
||||
out << " right: " << roomConnectionToYAML(room_data.right_room) << "\n";
|
||||
}
|
||||
|
||||
// --- Tilemap (MAP_HEIGHT filas × MAP_WIDTH columnas, formato flow) ---
|
||||
void RoomFormat::writeTilemapSection(std::ostringstream& out, const Room::Data& room_data) {
|
||||
out << "\n";
|
||||
out << "# Tilemap: " << Map::HEIGHT << " filas x " << Map::WIDTH << " columnas @ " << Tile::SIZE << "px/tile\n";
|
||||
out << "tilemap:\n";
|
||||
|
||||
// Mapa de dibujo
|
||||
out << " # Mapa de dibujo (indices de tiles, -1 = vacio)\n";
|
||||
out << " draw:\n";
|
||||
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;
|
||||
auto write_grid = [&out](const auto& tilemap, const char* header, int empty_value) {
|
||||
out << header;
|
||||
for (int row = 0; row < Map::HEIGHT; ++row) {
|
||||
out << " - [";
|
||||
for (int col = 0; col < Map::WIDTH; ++col) {
|
||||
const int INDEX = (row * Map::WIDTH) + col;
|
||||
out << (INDEX < static_cast<int>(tilemap.size()) ? tilemap[INDEX] : empty_value);
|
||||
if (col < Map::WIDTH - 1) { out << ", "; }
|
||||
}
|
||||
if (col < Map::WIDTH - 1) { out << ", "; }
|
||||
out << "]\n";
|
||||
}
|
||||
out << "]\n";
|
||||
}
|
||||
};
|
||||
|
||||
// Mapa de colisiones
|
||||
out << " # Mapa de colisiones (0 = vacio, 1 = solido)\n";
|
||||
out << " collision:\n";
|
||||
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.collision_tile_map.size())) {
|
||||
out << room_data.collision_tile_map[index];
|
||||
} else {
|
||||
out << 0;
|
||||
}
|
||||
if (col < Map::WIDTH - 1) { out << ", "; }
|
||||
}
|
||||
out << "]\n";
|
||||
}
|
||||
write_grid(room_data.tile_map, " # Mapa de dibujo (indices de tiles, -1 = vacio)\n draw:\n", -1);
|
||||
write_grid(room_data.collision_tile_map, " # Mapa de colisiones (0 = vacio, 1 = solido)\n collision:\n", 0);
|
||||
}
|
||||
|
||||
// --- Enemigos ---
|
||||
if (!room_data.enemies.empty()) {
|
||||
void RoomFormat::writeEnemiesSection(std::ostringstream& out, const Room::Data& room_data) {
|
||||
if (room_data.enemies.empty()) { return; }
|
||||
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";
|
||||
if (enemy.type != "path") { out << " type: " << enemy.type << "\n"; }
|
||||
|
||||
const int POS_X = static_cast<int>(std::round(enemy.x / Tile::SIZE));
|
||||
const 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";
|
||||
|
||||
const int B1_X = enemy.x1 / Tile::SIZE;
|
||||
const int B1_Y = enemy.y1 / Tile::SIZE;
|
||||
const int B2_X = enemy.x2 / Tile::SIZE;
|
||||
const 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.flip) { out << " flip: true\n"; }
|
||||
if (enemy.mirror) { out << " mirror: true\n"; }
|
||||
if (enemy.frame != -1) { out << " frame: " << enemy.frame << "\n"; }
|
||||
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";
|
||||
if (enemy.type != "path") { out << " type: " << enemy.type << "\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.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";
|
||||
void RoomFormat::writeItemsSection(std::ostringstream& out, const Room::Data& room_data) {
|
||||
if (room_data.items.empty()) { return; }
|
||||
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";
|
||||
const int ITEM_X = static_cast<int>(std::round(item.x / Tile::SIZE));
|
||||
const 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";
|
||||
}
|
||||
|
||||
// color1/color2 solo si son override explícito del default
|
||||
if (item.color1_overridden) {
|
||||
out << " color1: " << static_cast<int>(item.color1) << "\n";
|
||||
}
|
||||
if (item.color2_overridden) {
|
||||
out << " color2: " << static_cast<int>(item.color2) << "\n";
|
||||
}
|
||||
|
||||
out << "\n";
|
||||
}
|
||||
if (item.counter != 0) { out << " counter: " << item.counter << "\n"; }
|
||||
if (item.color1_overridden) { out << " color1: " << static_cast<int>(item.color1) << "\n"; }
|
||||
if (item.color2_overridden) { out << " color2: " << static_cast<int>(item.color2) << "\n"; }
|
||||
out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// --- Plataformas ---
|
||||
if (!room_data.platforms.empty()) {
|
||||
out << "# Plataformas móviles en esta habitación\n";
|
||||
out << "platforms:\n";
|
||||
for (const auto& plat : room_data.platforms) {
|
||||
out << " - animation: " << plat.animation_path << "\n";
|
||||
out << " speed: " << plat.speed << "\n";
|
||||
out << " loop: " << (plat.loop == LoopMode::CIRCULAR ? "circular" : "pingpong") << "\n";
|
||||
if (plat.easing != "linear") { out << " easing: " << plat.easing << "\n"; }
|
||||
if (plat.frame != -1) { out << " frame: " << plat.frame << "\n"; }
|
||||
out << " path:\n";
|
||||
for (const auto& wp : plat.path) {
|
||||
int wx = static_cast<int>(std::round(wp.x / Tile::SIZE));
|
||||
int wy = static_cast<int>(std::round(wp.y / Tile::SIZE));
|
||||
out << " - {x: " << wx << ", y: " << wy;
|
||||
if (wp.wait > 0.0F) { out << ", wait: " << wp.wait; }
|
||||
out << "}\n";
|
||||
}
|
||||
out << "\n";
|
||||
void RoomFormat::writePlatformsSection(std::ostringstream& out, const Room::Data& room_data) {
|
||||
if (room_data.platforms.empty()) { return; }
|
||||
out << "# Plataformas móviles en esta habitación\n";
|
||||
out << "platforms:\n";
|
||||
for (const auto& plat : room_data.platforms) {
|
||||
out << " - animation: " << plat.animation_path << "\n";
|
||||
out << " speed: " << plat.speed << "\n";
|
||||
out << " loop: " << (plat.loop == LoopMode::CIRCULAR ? "circular" : "pingpong") << "\n";
|
||||
if (plat.easing != "linear") { out << " easing: " << plat.easing << "\n"; }
|
||||
if (plat.frame != -1) { out << " frame: " << plat.frame << "\n"; }
|
||||
out << " path:\n";
|
||||
for (const auto& wp : plat.path) {
|
||||
const int WX = static_cast<int>(std::round(wp.x / Tile::SIZE));
|
||||
const int WY = static_cast<int>(std::round(wp.y / Tile::SIZE));
|
||||
out << " - {x: " << WX << ", y: " << WY;
|
||||
if (wp.wait > 0.0F) { out << ", wait: " << wp.wait; }
|
||||
out << "}\n";
|
||||
}
|
||||
out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// --- Llaves ---
|
||||
if (!room_data.keys.empty()) {
|
||||
out << "# Llaves en esta habitación\n";
|
||||
out << "keys:\n";
|
||||
for (const auto& key : room_data.keys) {
|
||||
out << " - animation: " << key.animation_path << "\n";
|
||||
out << " id: \"" << key.id << "\"\n";
|
||||
int kx = static_cast<int>(std::round(key.x / Tile::SIZE));
|
||||
int ky = static_cast<int>(std::round(key.y / Tile::SIZE));
|
||||
out << " position: {x: " << kx << ", y: " << ky << "}\n";
|
||||
out << "\n";
|
||||
}
|
||||
void RoomFormat::writeKeysSection(std::ostringstream& out, const Room::Data& room_data) {
|
||||
if (room_data.keys.empty()) { return; }
|
||||
out << "# Llaves en esta habitación\n";
|
||||
out << "keys:\n";
|
||||
for (const auto& key : room_data.keys) {
|
||||
out << " - animation: " << key.animation_path << "\n";
|
||||
out << " id: \"" << key.id << "\"\n";
|
||||
const int KX = static_cast<int>(std::round(key.x / Tile::SIZE));
|
||||
const int KY = static_cast<int>(std::round(key.y / Tile::SIZE));
|
||||
out << " position: {x: " << KX << ", y: " << KY << "}\n";
|
||||
out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// --- Puertas ---
|
||||
if (!room_data.doors.empty()) {
|
||||
out << "# Puertas en esta habitación\n";
|
||||
out << "doors:\n";
|
||||
for (const auto& door : room_data.doors) {
|
||||
out << " - animation: " << door.animation_path << "\n";
|
||||
out << " id: \"" << door.id << "\"\n";
|
||||
int dx = static_cast<int>(std::round(door.x / Tile::SIZE));
|
||||
int dy = static_cast<int>(std::round(door.y / Tile::SIZE));
|
||||
out << " position: {x: " << dx << ", y: " << dy << "}\n";
|
||||
out << "\n";
|
||||
}
|
||||
void RoomFormat::writeDoorsSection(std::ostringstream& out, const Room::Data& room_data) {
|
||||
if (room_data.doors.empty()) { return; }
|
||||
out << "# Puertas en esta habitación\n";
|
||||
out << "doors:\n";
|
||||
for (const auto& door : room_data.doors) {
|
||||
out << " - animation: " << door.animation_path << "\n";
|
||||
out << " id: \"" << door.id << "\"\n";
|
||||
const int DX = static_cast<int>(std::round(door.x / Tile::SIZE));
|
||||
const int DY = static_cast<int>(std::round(door.y / Tile::SIZE));
|
||||
out << " position: {x: " << DX << ", y: " << DY << "}\n";
|
||||
out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
auto RoomFormat::buildContent(const Room::Data& room_data) -> std::string {
|
||||
std::ostringstream out;
|
||||
writeRoomSection(out, room_data);
|
||||
writeTilemapSection(out, room_data);
|
||||
writeEnemiesSection(out, room_data);
|
||||
writeItemsSection(out, room_data);
|
||||
writePlatformsSection(out, room_data);
|
||||
writeKeysSection(out, room_data);
|
||||
writeDoorsSection(out, room_data);
|
||||
return out.str();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <string> // Para string
|
||||
#include <vector> // Para vector
|
||||
#include <sstream> // Para ostringstream
|
||||
#include <string> // Para string
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "external/fkyaml_node.hpp" // Para fkyaml::node
|
||||
#include "game/entities/door.hpp" // Para Door::Data
|
||||
@@ -69,7 +70,7 @@ class RoomFormat {
|
||||
// --- Parsing helpers (siempre disponibles, los usa loadYAML) ---
|
||||
static auto convertRoomConnection(const std::string& value) -> std::string;
|
||||
static auto convertAutoSurface(const fkyaml::node& node) -> int;
|
||||
static auto flattenTilemap(const std::vector<std::vector<int>>& tilemap_2d) -> std::vector<int>; // NOLINT(readability-avoid-const-params-in-decls)
|
||||
static auto flattenTilemap(const std::vector<std::vector<int>>& tilemap_2d) -> std::vector<int>;
|
||||
static void parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name);
|
||||
static void parseRoomConnections(const fkyaml::node& conn_node, Room::Data& room);
|
||||
static void parseTilemap(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose);
|
||||
@@ -90,5 +91,12 @@ class RoomFormat {
|
||||
// --- Serialization helpers (solo en debug, los usa saveYAML) ---
|
||||
static auto buildContent(const Room::Data& room_data) -> std::string;
|
||||
static auto roomConnectionToYAML(const std::string& connection) -> std::string;
|
||||
static void writeRoomSection(std::ostringstream& out, const Room::Data& room_data);
|
||||
static void writeTilemapSection(std::ostringstream& out, const Room::Data& room_data);
|
||||
static void writeEnemiesSection(std::ostringstream& out, const Room::Data& room_data);
|
||||
static void writeItemsSection(std::ostringstream& out, const Room::Data& room_data);
|
||||
static void writePlatformsSection(std::ostringstream& out, const Room::Data& room_data);
|
||||
static void writeKeysSection(std::ostringstream& out, const Room::Data& room_data);
|
||||
static void writeDoorsSection(std::ostringstream& out, const Room::Data& room_data);
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ auto RoomTracker::addRoom(const std::string& name) -> bool {
|
||||
if (!hasBeenVisited(name)) {
|
||||
// En caso contrario añádela a la lista
|
||||
rooms_.push_back(name);
|
||||
return true; // NOLINT(readability-simplify-boolean-expr)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -98,46 +98,41 @@ auto TileCollider::checkCeiling(float x, float y, float w) const -> float {
|
||||
// WALL: bloquea si los pies estaban por encima (como PASSABLE).
|
||||
// PASSABLE: solo si los pies estaban por encima del borde superior del tile.
|
||||
// SLOPE: siempre bloquea (las slopes son sólidas, como muros en diagonal).
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
||||
|
||||
// Calcula la y del suelo per a un tile concret. Retorna Collision::NONE si el tile
|
||||
// no actua com a suelo en aquest cas (segons el tipus i el rang del moviment).
|
||||
auto TileCollider::floorYForTile(Tile tile, int col, int row, float x, float w, float foot_y_current, float foot_y_new) const -> float {
|
||||
if (tile == Tile::WALL || tile == Tile::PASSABLE) {
|
||||
const float TILE_TOP = toPixel(row);
|
||||
// WALL/PASSABLE només compten si els peus estaven per damunt abans del moviment
|
||||
return (foot_y_current <= TILE_TOP) ? TILE_TOP : Collision::NONE;
|
||||
}
|
||||
if (tile == Tile::SLOPE_L || tile == Tile::SLOPE_R) {
|
||||
const float CHECK_X = (tile == Tile::SLOPE_L) ? x : x + w - 1;
|
||||
const float SLOPE_Y = getSlopeY(col, row, CHECK_X);
|
||||
// Slopes són sòlides: aterrar sempre que els peus arriben a la superfície
|
||||
return (foot_y_new >= SLOPE_Y) ? SLOPE_Y : Collision::NONE;
|
||||
}
|
||||
return Collision::NONE;
|
||||
}
|
||||
|
||||
auto TileCollider::checkFloor(float x, float foot_y_current, float w, float foot_y_new) const -> FloorHit {
|
||||
int start_row = toTile(static_cast<int>(foot_y_current));
|
||||
int end_row = toTile(static_cast<int>(foot_y_new));
|
||||
int left_col = toTile(static_cast<int>(x));
|
||||
int right_col = toTile(static_cast<int>(x + w - 1));
|
||||
const int START_ROW = toTile(static_cast<int>(foot_y_current));
|
||||
const int END_ROW = toTile(static_cast<int>(foot_y_new));
|
||||
const int LEFT_COL = toTile(static_cast<int>(x));
|
||||
const int RIGHT_COL = toTile(static_cast<int>(x + w - 1));
|
||||
|
||||
FloorHit best;
|
||||
|
||||
for (int row = start_row; row <= end_row; ++row) {
|
||||
for (int col = left_col; col <= right_col; ++col) {
|
||||
auto tile = getTileAt(col, row);
|
||||
float floor_y = Collision::NONE;
|
||||
|
||||
if (tile == Tile::WALL) {
|
||||
float tile_top = toPixel(row);
|
||||
if (foot_y_current <= tile_top) {
|
||||
floor_y = tile_top;
|
||||
}
|
||||
} else if (tile == Tile::PASSABLE) {
|
||||
float tile_top = toPixel(row);
|
||||
// Solo cuenta como suelo si los pies estaban por encima antes del movimiento
|
||||
if (foot_y_current <= tile_top) {
|
||||
floor_y = tile_top;
|
||||
}
|
||||
} else if (tile == Tile::SLOPE_L || tile == Tile::SLOPE_R) {
|
||||
float check_x = (tile == Tile::SLOPE_L) ? x : x + w - 1;
|
||||
float slope_y = getSlopeY(col, row, check_x);
|
||||
// Slopes son sólidas: aterrizar siempre que los pies lleguen a la superficie
|
||||
if (foot_y_new >= slope_y) {
|
||||
floor_y = slope_y;
|
||||
}
|
||||
}
|
||||
|
||||
if (floor_y != Collision::NONE && (best.y == Collision::NONE || floor_y < best.y)) {
|
||||
best = {.y = floor_y, .type = tile, .tile_x = col, .tile_y = row};
|
||||
for (int row = START_ROW; row <= END_ROW; ++row) {
|
||||
for (int col = LEFT_COL; col <= RIGHT_COL; ++col) {
|
||||
const auto TILE = getTileAt(col, row);
|
||||
const float FLOOR_Y = floorYForTile(TILE, col, row, x, w, foot_y_current, foot_y_new);
|
||||
if (FLOOR_Y == Collision::NONE) { continue; }
|
||||
if (best.y == Collision::NONE || FLOOR_Y < best.y) {
|
||||
best = {.y = FLOOR_Y, .type = TILE, .tile_x = col, .tile_y = row};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
@@ -196,7 +191,7 @@ auto TileCollider::isInsideAnySlope(float x, float foot_y, float w) const -> boo
|
||||
// Busca una slope directamente debajo del jugador (para transición ground→slope).
|
||||
// Escanea la fila de los pies Y la fila superior: las slopes en escalera siempre
|
||||
// tienen el tile de entrada una fila arriba del suelo desde el que se accede.
|
||||
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
|
||||
|
||||
auto TileCollider::checkSlopeBelow(float x, float foot_y, float w) const -> SlopeInfo {
|
||||
int foot_row = toTile(static_cast<int>(foot_y));
|
||||
int left_col = toTile(static_cast<int>(x));
|
||||
|
||||
@@ -70,4 +70,7 @@ class TileCollider {
|
||||
int border_px_; // Offset en píxeles (CollisionBorder::PX)
|
||||
|
||||
const std::vector<int>& tile_map_;
|
||||
|
||||
// Calcula la y del suelo per a un tile concret (helper de checkFloor)
|
||||
[[nodiscard]] auto floorYForTile(Tile tile, int col, int row, float x, float w, float foot_y_current, float foot_y_new) const -> float;
|
||||
};
|
||||
|
||||
+126
-224
@@ -2,9 +2,11 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para std::clamp, std::ranges::transform
|
||||
#include <filesystem> // Para create_directories
|
||||
#include <fstream> // Para ifstream, ofstream
|
||||
#include <iostream> // Para cout, cerr
|
||||
#include <iterator> // Para std::back_inserter
|
||||
#include <string> // Para string
|
||||
#include <unordered_map> // Para unordered_map
|
||||
|
||||
@@ -517,90 +519,65 @@ namespace Options {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Llig parent[key] cap a dst si existeix; ignora errors de format (conserva el default).
|
||||
template <typename T>
|
||||
void readYamlField(const fkyaml::node& parent, const char* key, T& dst) {
|
||||
if (!parent.contains(key)) { return; }
|
||||
try {
|
||||
dst = parent[key].template get_value<T>();
|
||||
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
|
||||
}
|
||||
}
|
||||
|
||||
// Versió específica per a volums (clamp a [0,1])
|
||||
void readYamlVolume(const fkyaml::node& parent, const char* key, float& dst) {
|
||||
if (!parent.contains(key)) { return; }
|
||||
try {
|
||||
dst = std::clamp(parent[key].get_value<float>(), 0.0F, 1.0F);
|
||||
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
|
||||
}
|
||||
}
|
||||
|
||||
// Versió específica per a colors de paleta (clamp a [0,255])
|
||||
void readYamlPaletteIndex(const fkyaml::node& parent, const char* key, int& dst) {
|
||||
if (!parent.contains(key)) { return; }
|
||||
try {
|
||||
dst = std::clamp(parent[key].get_value<int>(), 0, 255);
|
||||
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Carga configuración de audio desde YAML
|
||||
void loadAudioConfigFromYaml(const fkyaml::node& yaml) { // NOLINT(readability-function-cognitive-complexity)
|
||||
void loadAudioConfigFromYaml(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("audio")) { return; }
|
||||
const auto& a = yaml["audio"];
|
||||
|
||||
if (a.contains("enabled")) {
|
||||
try {
|
||||
audio.enabled = a["enabled"].get_value<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
if (a.contains("volume")) {
|
||||
try {
|
||||
audio.volume = std::clamp(a["volume"].get_value<float>(), 0.0F, 1.0F);
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
readYamlField(a, "enabled", audio.enabled);
|
||||
readYamlVolume(a, "volume", audio.volume);
|
||||
if (a.contains("music")) {
|
||||
const auto& m = a["music"];
|
||||
if (m.contains("enabled")) {
|
||||
try {
|
||||
audio.music.enabled = m["enabled"].get_value<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
if (m.contains("volume")) {
|
||||
try {
|
||||
audio.music.volume = std::clamp(m["volume"].get_value<float>(), 0.0F, 1.0F);
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
readYamlField(m, "enabled", audio.music.enabled);
|
||||
readYamlVolume(m, "volume", audio.music.volume);
|
||||
}
|
||||
if (a.contains("sound")) {
|
||||
const auto& s = a["sound"];
|
||||
if (s.contains("enabled")) {
|
||||
try {
|
||||
audio.sound.enabled = s["enabled"].get_value<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
if (s.contains("volume")) {
|
||||
try {
|
||||
audio.sound.volume = std::clamp(s["volume"].get_value<float>(), 0.0F, 1.0F);
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
readYamlField(s, "enabled", audio.sound.enabled);
|
||||
readYamlVolume(s, "volume", audio.sound.volume);
|
||||
}
|
||||
}
|
||||
|
||||
// Carga configuración de la consola desde YAML
|
||||
void loadConsoleConfigFromYaml(const fkyaml::node& yaml) { // NOLINT(readability-function-cognitive-complexity)
|
||||
void loadConsoleConfigFromYaml(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("console")) { return; }
|
||||
const auto& c = yaml["console"];
|
||||
|
||||
if (c.contains("transparent")) {
|
||||
try {
|
||||
console.transparent = c["transparent"].get_value<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
if (c.contains("bg_color")) {
|
||||
try {
|
||||
console.bg_color = std::clamp(c["bg_color"].get_value<int>(), 0, 255);
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
if (c.contains("msg_color")) {
|
||||
try {
|
||||
console.msg_color = std::clamp(c["msg_color"].get_value<int>(), 0, 255);
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
if (c.contains("prompt_color")) {
|
||||
try {
|
||||
console.prompt_color = std::clamp(c["prompt_color"].get_value<int>(), 0, 255);
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
if (c.contains("command_color")) {
|
||||
try {
|
||||
console.command_color = std::clamp(c["command_color"].get_value<int>(), 0, 255);
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
readYamlField(c, "transparent", console.transparent);
|
||||
readYamlPaletteIndex(c, "bg_color", console.bg_color);
|
||||
readYamlPaletteIndex(c, "msg_color", console.msg_color);
|
||||
readYamlPaletteIndex(c, "prompt_color", console.prompt_color);
|
||||
readYamlPaletteIndex(c, "command_color", console.command_color);
|
||||
}
|
||||
|
||||
// Carga configuración de la pantalla de carga de recursos desde YAML
|
||||
@@ -1048,110 +1025,91 @@ namespace Options {
|
||||
crtpi_file_path = path;
|
||||
}
|
||||
|
||||
// Defaults dels 4 presets CrtPi (DEFAULT, CURVED, SHARP, MINIMAL)
|
||||
static auto defaultCrtPiPresets() -> std::vector<CrtPiPreset> {
|
||||
return {
|
||||
{.name = "DEFAULT", .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 = true, .enable_multisample = true, .enable_gamma = true, .enable_curvature = false, .enable_sharper = false},
|
||||
{.name = "CURVED", .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 = true, .enable_multisample = true, .enable_gamma = true, .enable_curvature = true, .enable_sharper = false},
|
||||
{.name = "SHARP", .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 = true, .enable_multisample = false, .enable_gamma = true, .enable_curvature = false, .enable_sharper = true},
|
||||
{.name = "MINIMAL", .scanline_weight = 8.0F, .scanline_gap_brightness = 0.05F, .bloom_factor = 2.0F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 1.00F, .curvature_x = 0.0F, .curvature_y = 0.0F, .mask_type = 0, .enable_scanlines = true, .enable_multisample = false, .enable_gamma = false, .enable_curvature = false, .enable_sharper = false}};
|
||||
}
|
||||
|
||||
// Escriu el fitxer CrtPi amb capçalera + els presets default. Retorna false si no pot obrir.
|
||||
static auto writeCrtPiDefaultFile(const std::string& path, const std::vector<CrtPiPreset>& presets) -> bool {
|
||||
const std::filesystem::path P(path);
|
||||
if (P.has_parent_path()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(P.parent_path(), ec);
|
||||
}
|
||||
std::ofstream out(path);
|
||||
if (!out.is_open()) { return false; }
|
||||
|
||||
out << "# Projecte 2026 - CrtPi Shader Presets\n";
|
||||
out << "# scanline_weight: ajuste gaussiano (mayor = scanlines mas estrechas, default 6.0)\n";
|
||||
out << "# scanline_gap_brightness: brillo minimo entre scanlines (0.0-1.0, default 0.12)\n";
|
||||
out << "# bloom_factor: factor de brillo para zonas iluminadas (default 3.5)\n";
|
||||
out << "# input_gamma: gamma de entrada - linealizacion (default 2.4)\n";
|
||||
out << "# output_gamma: gamma de salida - codificacion (default 2.2)\n";
|
||||
out << "# mask_brightness: brillo sub-pixeles de la mascara de fosforo (default 0.80)\n";
|
||||
out << "# curvature_x/y: distorsion barrel CRT (0.0 = plana)\n";
|
||||
out << "# mask_type: 0=ninguna, 1=verde/magenta, 2=RGB fosforo\n";
|
||||
out << "# enable_scanlines/multisample/gamma/curvature/sharper: true/false\n";
|
||||
out << "\npresets:\n";
|
||||
for (const auto& p : presets) {
|
||||
out << " - name: \"" << p.name << "\"\n";
|
||||
out << " scanline_weight: " << p.scanline_weight << "\n";
|
||||
out << " scanline_gap_brightness: " << p.scanline_gap_brightness << "\n";
|
||||
out << " bloom_factor: " << p.bloom_factor << "\n";
|
||||
out << " input_gamma: " << p.input_gamma << "\n";
|
||||
out << " output_gamma: " << p.output_gamma << "\n";
|
||||
out << " mask_brightness: " << p.mask_brightness << "\n";
|
||||
out << " curvature_x: " << p.curvature_x << "\n";
|
||||
out << " curvature_y: " << p.curvature_y << "\n";
|
||||
out << " mask_type: " << p.mask_type << "\n";
|
||||
out << " enable_scanlines: " << (p.enable_scanlines ? "true" : "false") << "\n";
|
||||
out << " enable_multisample: " << (p.enable_multisample ? "true" : "false") << "\n";
|
||||
out << " enable_gamma: " << (p.enable_gamma ? "true" : "false") << "\n";
|
||||
out << " enable_curvature: " << (p.enable_curvature ? "true" : "false") << "\n";
|
||||
out << " enable_sharper: " << (p.enable_sharper ? "true" : "false") << "\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parseja un node YAML a un CrtPiPreset usant els helpers genèrics
|
||||
static auto parseCrtPiPreset(const fkyaml::node& p) -> CrtPiPreset {
|
||||
CrtPiPreset preset;
|
||||
readYamlField(p, "name", preset.name);
|
||||
parseFloatField(p, "scanline_weight", preset.scanline_weight);
|
||||
parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness);
|
||||
parseFloatField(p, "bloom_factor", preset.bloom_factor);
|
||||
parseFloatField(p, "input_gamma", preset.input_gamma);
|
||||
parseFloatField(p, "output_gamma", preset.output_gamma);
|
||||
parseFloatField(p, "mask_brightness", preset.mask_brightness);
|
||||
parseFloatField(p, "curvature_x", preset.curvature_x);
|
||||
parseFloatField(p, "curvature_y", preset.curvature_y);
|
||||
readYamlField(p, "mask_type", preset.mask_type);
|
||||
readYamlField(p, "enable_scanlines", preset.enable_scanlines);
|
||||
readYamlField(p, "enable_multisample", preset.enable_multisample);
|
||||
readYamlField(p, "enable_gamma", preset.enable_gamma);
|
||||
readYamlField(p, "enable_curvature", preset.enable_curvature);
|
||||
readYamlField(p, "enable_sharper", preset.enable_sharper);
|
||||
return preset;
|
||||
}
|
||||
|
||||
// Carga los presets del shader CrtPi desde el fichero. Crea defaults si no existe.
|
||||
auto loadCrtPiFromFile() -> bool { // NOLINT(readability-function-cognitive-complexity)
|
||||
auto loadCrtPiFromFile() -> bool {
|
||||
crtpi_presets.clear();
|
||||
|
||||
std::ifstream file(crtpi_file_path);
|
||||
if (!file.good()) {
|
||||
std::cout << "CrtPi file not found, creating default: " << crtpi_file_path << '\n';
|
||||
// Crear directorio padre si no existe
|
||||
const std::filesystem::path P(crtpi_file_path);
|
||||
if (P.has_parent_path()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(P.parent_path(), ec);
|
||||
}
|
||||
// Escribir defaults
|
||||
std::ofstream out(crtpi_file_path);
|
||||
if (!out.is_open()) {
|
||||
std::cerr << "Error: Cannot create CrtPi file: " << crtpi_file_path << '\n';
|
||||
// Cargar defaults en memoria aunque no se pueda escribir
|
||||
crtpi_presets.push_back({"DEFAULT", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false});
|
||||
crtpi_presets.push_back({"CURVED", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, true, false});
|
||||
crtpi_presets.push_back({"SHARP", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, false, true, false, true});
|
||||
crtpi_presets.push_back({"MINIMAL", 8.0F, 0.05F, 2.0F, 2.4F, 2.2F, 1.00F, 0.0F, 0.0F, 0, true, false, false, false, false});
|
||||
video.shader.current_crtpi_preset = 0;
|
||||
return true;
|
||||
}
|
||||
out << "# Projecte 2026 - CrtPi Shader Presets\n";
|
||||
out << "# scanline_weight: ajuste gaussiano (mayor = scanlines mas estrechas, default 6.0)\n";
|
||||
out << "# scanline_gap_brightness: brillo minimo entre scanlines (0.0-1.0, default 0.12)\n";
|
||||
out << "# bloom_factor: factor de brillo para zonas iluminadas (default 3.5)\n";
|
||||
out << "# input_gamma: gamma de entrada - linealizacion (default 2.4)\n";
|
||||
out << "# output_gamma: gamma de salida - codificacion (default 2.2)\n";
|
||||
out << "# mask_brightness: brillo sub-pixeles de la mascara de fosforo (default 0.80)\n";
|
||||
out << "# curvature_x/y: distorsion barrel CRT (0.0 = plana)\n";
|
||||
out << "# mask_type: 0=ninguna, 1=verde/magenta, 2=RGB fosforo\n";
|
||||
out << "# enable_scanlines/multisample/gamma/curvature/sharper: true/false\n";
|
||||
out << "\n";
|
||||
out << "presets:\n";
|
||||
out << " - name: \"DEFAULT\"\n";
|
||||
out << " scanline_weight: 6.0\n";
|
||||
out << " scanline_gap_brightness: 0.12\n";
|
||||
out << " bloom_factor: 3.5\n";
|
||||
out << " input_gamma: 2.4\n";
|
||||
out << " output_gamma: 2.2\n";
|
||||
out << " mask_brightness: 0.80\n";
|
||||
out << " curvature_x: 0.05\n";
|
||||
out << " curvature_y: 0.10\n";
|
||||
out << " mask_type: 2\n";
|
||||
out << " enable_scanlines: true\n";
|
||||
out << " enable_multisample: true\n";
|
||||
out << " enable_gamma: true\n";
|
||||
out << " enable_curvature: false\n";
|
||||
out << " enable_sharper: false\n";
|
||||
out << " - name: \"CURVED\"\n";
|
||||
out << " scanline_weight: 6.0\n";
|
||||
out << " scanline_gap_brightness: 0.12\n";
|
||||
out << " bloom_factor: 3.5\n";
|
||||
out << " input_gamma: 2.4\n";
|
||||
out << " output_gamma: 2.2\n";
|
||||
out << " mask_brightness: 0.80\n";
|
||||
out << " curvature_x: 0.05\n";
|
||||
out << " curvature_y: 0.10\n";
|
||||
out << " mask_type: 2\n";
|
||||
out << " enable_scanlines: true\n";
|
||||
out << " enable_multisample: true\n";
|
||||
out << " enable_gamma: true\n";
|
||||
out << " enable_curvature: true\n";
|
||||
out << " enable_sharper: false\n";
|
||||
out << " - name: \"SHARP\"\n";
|
||||
out << " scanline_weight: 6.0\n";
|
||||
out << " scanline_gap_brightness: 0.12\n";
|
||||
out << " bloom_factor: 3.5\n";
|
||||
out << " input_gamma: 2.4\n";
|
||||
out << " output_gamma: 2.2\n";
|
||||
out << " mask_brightness: 0.80\n";
|
||||
out << " curvature_x: 0.05\n";
|
||||
out << " curvature_y: 0.10\n";
|
||||
out << " mask_type: 2\n";
|
||||
out << " enable_scanlines: true\n";
|
||||
out << " enable_multisample: false\n";
|
||||
out << " enable_gamma: true\n";
|
||||
out << " enable_curvature: false\n";
|
||||
out << " enable_sharper: true\n";
|
||||
out << " - name: \"MINIMAL\"\n";
|
||||
out << " scanline_weight: 8.0\n";
|
||||
out << " scanline_gap_brightness: 0.05\n";
|
||||
out << " bloom_factor: 2.0\n";
|
||||
out << " input_gamma: 2.4\n";
|
||||
out << " output_gamma: 2.2\n";
|
||||
out << " mask_brightness: 1.00\n";
|
||||
out << " curvature_x: 0.0\n";
|
||||
out << " curvature_y: 0.0\n";
|
||||
out << " mask_type: 0\n";
|
||||
out << " enable_scanlines: true\n";
|
||||
out << " enable_multisample: false\n";
|
||||
out << " enable_gamma: false\n";
|
||||
out << " enable_curvature: false\n";
|
||||
out << " enable_sharper: false\n";
|
||||
out.close();
|
||||
std::cout << "CrtPi file created with defaults: " << crtpi_file_path << '\n';
|
||||
crtpi_presets.push_back({"DEFAULT", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false});
|
||||
crtpi_presets.push_back({"CURVED", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, true, false});
|
||||
crtpi_presets.push_back({"SHARP", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, false, true, false, true});
|
||||
crtpi_presets.push_back({"MINIMAL", 8.0F, 0.05F, 2.0F, 2.4F, 2.2F, 1.00F, 0.0F, 0.0F, 0, true, false, false, false, false});
|
||||
crtpi_presets = defaultCrtPiPresets();
|
||||
video.shader.current_crtpi_preset = 0;
|
||||
if (!writeCrtPiDefaultFile(crtpi_file_path, crtpi_presets)) {
|
||||
std::cerr << "Error: Cannot create CrtPi file: " << crtpi_file_path << '\n';
|
||||
return true; // defaults en memòria igualment
|
||||
}
|
||||
std::cout << "CrtPi file created with defaults: " << crtpi_file_path << '\n';
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1160,77 +1118,21 @@ namespace Options {
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
|
||||
if (yaml.contains("presets")) {
|
||||
const auto& presets = yaml["presets"];
|
||||
for (const auto& p : presets) {
|
||||
CrtPiPreset preset;
|
||||
if (p.contains("name")) {
|
||||
preset.name = p["name"].get_value<std::string>();
|
||||
}
|
||||
parseFloatField(p, "scanline_weight", preset.scanline_weight);
|
||||
parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness);
|
||||
parseFloatField(p, "bloom_factor", preset.bloom_factor);
|
||||
parseFloatField(p, "input_gamma", preset.input_gamma);
|
||||
parseFloatField(p, "output_gamma", preset.output_gamma);
|
||||
parseFloatField(p, "mask_brightness", preset.mask_brightness);
|
||||
parseFloatField(p, "curvature_x", preset.curvature_x);
|
||||
parseFloatField(p, "curvature_y", preset.curvature_y);
|
||||
if (p.contains("mask_type")) {
|
||||
try {
|
||||
preset.mask_type = p["mask_type"].get_value<int>();
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
if (p.contains("enable_scanlines")) {
|
||||
try {
|
||||
preset.enable_scanlines = p["enable_scanlines"].get_value<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
if (p.contains("enable_multisample")) {
|
||||
try {
|
||||
preset.enable_multisample = p["enable_multisample"].get_value<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
if (p.contains("enable_gamma")) {
|
||||
try {
|
||||
preset.enable_gamma = p["enable_gamma"].get_value<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
if (p.contains("enable_curvature")) {
|
||||
try {
|
||||
preset.enable_curvature = p["enable_curvature"].get_value<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
if (p.contains("enable_sharper")) {
|
||||
try {
|
||||
preset.enable_sharper = p["enable_sharper"].get_value<bool>();
|
||||
} catch (...) { /* @INTENTIONAL: campo yaml ausente o malformado → dejar default */
|
||||
}
|
||||
}
|
||||
crtpi_presets.push_back(preset);
|
||||
}
|
||||
const auto& presets_node = yaml["presets"];
|
||||
std::ranges::transform(presets_node, std::back_inserter(crtpi_presets), parseCrtPiPreset);
|
||||
}
|
||||
|
||||
// Resolver el nombre del preset a índice
|
||||
if (!crtpi_presets.empty()) {
|
||||
resolveCrtPiPresetName();
|
||||
} else {
|
||||
video.shader.current_crtpi_preset = 0;
|
||||
}
|
||||
|
||||
std::cout << "CrtPi file loaded: " << crtpi_presets.size() << " preset(s)\n";
|
||||
return true;
|
||||
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "Error parsing CrtPi YAML: " << e.what() << '\n';
|
||||
// Cargar defaults en memoria en caso de error
|
||||
crtpi_presets.clear();
|
||||
crtpi_presets.push_back({"DEFAULT", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false});
|
||||
crtpi_presets.push_back(defaultCrtPiPresets().front()); // només DEFAULT en cas d'error
|
||||
video.shader.current_crtpi_preset = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
+33
-35
@@ -798,36 +798,41 @@ auto Game::getOrCreateRoom(const std::string& room_path) -> std::shared_ptr<Room
|
||||
return room;
|
||||
}
|
||||
|
||||
// Obtiene el collision tilemap de una room adyacente (nullptr si no existe)
|
||||
auto Game::getAdjacentCollision(Room::Border border) -> const std::vector<int>* {
|
||||
auto name = room_->getRoom(border);
|
||||
if (name == "0") { return nullptr; }
|
||||
return &getOrCreateRoom(name)->getCollisionTileMap();
|
||||
}
|
||||
|
||||
// Obtiene el collision tilemap de una room diagonal (A→B o C→D)
|
||||
auto Game::getDiagCollision(Room::Border first, Room::Border second) -> const std::vector<int>* {
|
||||
// Camino 1: room en dirección 'first', luego su adyacente en dirección 'second'
|
||||
auto name1 = room_->getRoom(first);
|
||||
if (name1 != "0") {
|
||||
auto r = getOrCreateRoom(name1);
|
||||
auto name2 = r->getRoom(second);
|
||||
if (name2 != "0") { return &getOrCreateRoom(name2)->getCollisionTileMap(); }
|
||||
}
|
||||
// Camino 2: room en dirección 'second', luego su adyacente en dirección 'first'
|
||||
auto name3 = room_->getRoom(second);
|
||||
if (name3 != "0") {
|
||||
auto r = getOrCreateRoom(name3);
|
||||
auto name4 = r->getRoom(first);
|
||||
if (name4 != "0") { return &getOrCreateRoom(name4)->getCollisionTileMap(); }
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// SolidActorManager de la room adyacente (nullptr si no existe)
|
||||
auto Game::getAdjacentSolidActors(Room::Border border) -> SolidActorManager* {
|
||||
auto name = room_->getRoom(border);
|
||||
if (name == "0") { return nullptr; }
|
||||
return &getOrCreateRoom(name)->getSolidActors();
|
||||
}
|
||||
|
||||
// Construye el tilemap extendido de la room actual con los bordes de las adyacentes
|
||||
void Game::buildCollisionBorders() {
|
||||
// NOLINTBEGIN(readability-identifier-naming) -- lambdas locales: se leen como llamadas a función, no son constantes.
|
||||
// Helper: obtiene el collision tilemap de una room adyacente (nullptr si no existe)
|
||||
auto getAdjacentCollision = [&](Room::Border b) -> const std::vector<int>* {
|
||||
auto name = room_->getRoom(b);
|
||||
if (name == "0") { return nullptr; }
|
||||
return &getOrCreateRoom(name)->getCollisionTileMap();
|
||||
};
|
||||
|
||||
// Helper: obtiene el collision tilemap de una room diagonal (A→B o C→D)
|
||||
auto getDiagCollision = [&](Room::Border first, Room::Border second) -> const std::vector<int>* {
|
||||
// Camino 1: room en dirección 'first', luego su adyacente en dirección 'second'
|
||||
auto name1 = room_->getRoom(first);
|
||||
if (name1 != "0") {
|
||||
auto r = getOrCreateRoom(name1);
|
||||
auto name2 = r->getRoom(second);
|
||||
if (name2 != "0") { return &getOrCreateRoom(name2)->getCollisionTileMap(); }
|
||||
}
|
||||
// Camino 2: room en dirección 'second', luego su adyacente en dirección 'first'
|
||||
auto name3 = room_->getRoom(second);
|
||||
if (name3 != "0") {
|
||||
auto r = getOrCreateRoom(name3);
|
||||
auto name4 = r->getRoom(first);
|
||||
if (name4 != "0") { return &getOrCreateRoom(name4)->getCollisionTileMap(); }
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
|
||||
CollisionMap::AdjacentData adj;
|
||||
adj.top = getAdjacentCollision(Room::Border::TOP);
|
||||
adj.bottom = getAdjacentCollision(Room::Border::BOTTOM);
|
||||
@@ -845,13 +850,6 @@ void Game::buildCollisionBorders() {
|
||||
// que los sweeps del Player vean AABBs dinámicos (puertas, plataformas)
|
||||
// de la room vecina cuando está cerca del borde, sin tener que esperar
|
||||
// a una transición completa de room.
|
||||
// NOLINTNEXTLINE(readability-identifier-naming) -- lambda local: se lee como función, no es constante.
|
||||
auto getAdjacentSolidActors = [&](Room::Border b) -> SolidActorManager* {
|
||||
auto name = room_->getRoom(b);
|
||||
if (name == "0") { return nullptr; }
|
||||
return &getOrCreateRoom(name)->getSolidActors();
|
||||
};
|
||||
|
||||
SolidActorManager::AdjacentActors sadj;
|
||||
sadj.upper = getAdjacentSolidActors(Room::Border::TOP);
|
||||
sadj.lower = getAdjacentSolidActors(Room::Border::BOTTOM);
|
||||
|
||||
+33
-30
@@ -57,36 +57,39 @@ class Game {
|
||||
};
|
||||
|
||||
// --- Métodos ---
|
||||
static void handleEvents(); // Comprueba los eventos de la cola
|
||||
void transitionToState(State new_state); // Cambia al estado especificado y resetea los timers
|
||||
void updatePlaying(float delta_time); // Actualiza el juego en estado PLAYING
|
||||
void updateBlackScreen(float delta_time); // Actualiza el juego en estado BLACK_SCREEN
|
||||
void updateGameOver(float delta_time); // Actualiza el juego en estado GAME_OVER
|
||||
void updateFadeToEnding(float delta_time); // Actualiza el juego en estado FADE_TO_ENDING
|
||||
void updatePostFadeEnding(float delta_time); // Actualiza el juego en estado POST_FADE_ENDING
|
||||
void renderPlaying(); // Renderiza el juego en estado PLAYING (directo a pantalla)
|
||||
static void renderBlackScreen(); // Renderiza el juego en estado BLACK_SCREEN (pantalla negra)
|
||||
static void renderGameOver(); // Renderiza el juego en estado GAME_OVER (pantalla negra)
|
||||
void renderFadeToEnding(); // Renderiza el juego en estado FADE_TO_ENDING (via backbuffer)
|
||||
static void renderPostFadeEnding(); // Renderiza el juego en estado POST_FADE_ENDING (pantalla negra)
|
||||
auto changeRoom(const std::string& room_path) -> bool; // Cambia de habitación
|
||||
auto getOrCreateRoom(const std::string& room_path) -> std::shared_ptr<Room>; // Obtiene una habitación del caché o la crea
|
||||
void buildCollisionBorders(); // Rellena el tilemap extendido de la room actual con bordes adyacentes
|
||||
void updateAdjacentRooms(float delta_time); // Actualiza enemigos de las habitaciones adyacentes
|
||||
void handleInput(); // Comprueba el teclado
|
||||
void checkPlayerIsOnBorder(); // Comprueba si el jugador esta en el borde de la pantalla y actua
|
||||
auto checkPlayerAndEnemies() -> bool; // Comprueba las colisiones del jugador con los enemigos
|
||||
void checkPlayerAndItems(); // Comprueba las colisiones del jugador con los objetos
|
||||
void checkPlayerAndKeys(); // Comprueba las colisiones del jugador con las llaves
|
||||
void checkIfPlayerIsAlive(); // Comprueba si el jugador esta vivo
|
||||
void killPlayer(); // Mata al jugador
|
||||
void togglePause(); // Pone el juego en pausa
|
||||
void initPlayer(const Player::SpawnData& spawn_point, std::shared_ptr<Room> room); // Inicializa al jugador
|
||||
void endTransition(); // Finaliza la transición entre pantallas
|
||||
void keepMusicPlaying(); // Hace sonar la música (safety net, delega en updateMusicForRoom)
|
||||
void updateMusicForRoom(); // Sincroniza la música con la zona de la room actual
|
||||
void demoInit(); // DEMO MODE: Inicializa las variables para el modo demo
|
||||
void demoCheckRoomChange(float delta_time); // DEMO MODE: Comprueba si se ha de cambiar de habitación
|
||||
static void handleEvents(); // Comprueba los eventos de la cola
|
||||
void transitionToState(State new_state); // Cambia al estado especificado y resetea los timers
|
||||
void updatePlaying(float delta_time); // Actualiza el juego en estado PLAYING
|
||||
void updateBlackScreen(float delta_time); // Actualiza el juego en estado BLACK_SCREEN
|
||||
void updateGameOver(float delta_time); // Actualiza el juego en estado GAME_OVER
|
||||
void updateFadeToEnding(float delta_time); // Actualiza el juego en estado FADE_TO_ENDING
|
||||
void updatePostFadeEnding(float delta_time); // Actualiza el juego en estado POST_FADE_ENDING
|
||||
void renderPlaying(); // Renderiza el juego en estado PLAYING (directo a pantalla)
|
||||
static void renderBlackScreen(); // Renderiza el juego en estado BLACK_SCREEN (pantalla negra)
|
||||
static void renderGameOver(); // Renderiza el juego en estado GAME_OVER (pantalla negra)
|
||||
void renderFadeToEnding(); // Renderiza el juego en estado FADE_TO_ENDING (via backbuffer)
|
||||
static void renderPostFadeEnding(); // Renderiza el juego en estado POST_FADE_ENDING (pantalla negra)
|
||||
auto changeRoom(const std::string& room_path) -> bool; // Cambia de habitación
|
||||
auto getOrCreateRoom(const std::string& room_path) -> std::shared_ptr<Room>; // Obtiene una habitación del caché o la crea
|
||||
void buildCollisionBorders(); // Rellena el tilemap extendido de la room actual con bordes adyacentes
|
||||
auto getAdjacentCollision(Room::Border border) -> const std::vector<int>*; // Collision tilemap de la room adyacente o nullptr
|
||||
auto getDiagCollision(Room::Border first, Room::Border second) -> const std::vector<int>*; // Collision tilemap de la room diagonal o nullptr
|
||||
auto getAdjacentSolidActors(Room::Border border) -> SolidActorManager*; // SolidActorManager de la room adyacente o nullptr
|
||||
void updateAdjacentRooms(float delta_time); // Actualiza enemigos de las habitaciones adyacentes
|
||||
void handleInput(); // Comprueba el teclado
|
||||
void checkPlayerIsOnBorder(); // Comprueba si el jugador esta en el borde de la pantalla y actua
|
||||
auto checkPlayerAndEnemies() -> bool; // Comprueba las colisiones del jugador con los enemigos
|
||||
void checkPlayerAndItems(); // Comprueba las colisiones del jugador con los objetos
|
||||
void checkPlayerAndKeys(); // Comprueba las colisiones del jugador con las llaves
|
||||
void checkIfPlayerIsAlive(); // Comprueba si el jugador esta vivo
|
||||
void killPlayer(); // Mata al jugador
|
||||
void togglePause(); // Pone el juego en pausa
|
||||
void initPlayer(const Player::SpawnData& spawn_point, std::shared_ptr<Room> room); // Inicializa al jugador
|
||||
void endTransition(); // Finaliza la transición entre pantallas
|
||||
void keepMusicPlaying(); // Hace sonar la música (safety net, delega en updateMusicForRoom)
|
||||
void updateMusicForRoom(); // Sincroniza la música con la zona de la room actual
|
||||
void demoInit(); // DEMO MODE: Inicializa las variables para el modo demo
|
||||
void demoCheckRoomChange(float delta_time); // DEMO MODE: Comprueba si se ha de cambiar de habitación
|
||||
#ifdef _DEBUG
|
||||
static void renderDebugInfo(); // Pone la información de debug en pantalla
|
||||
void handleDebugEvents(const SDL_Event& event); // Comprueba los eventos
|
||||
|
||||
@@ -47,7 +47,7 @@ Title::Title()
|
||||
}
|
||||
|
||||
// Destructor
|
||||
Title::~Title() { // NOLINT(modernize-use-equals-default)
|
||||
Title::~Title() {
|
||||
title_surface_->resetSubPalette();
|
||||
}
|
||||
|
||||
|
||||
+166
-149
@@ -193,84 +193,92 @@ void Console::redrawText() {
|
||||
Screen::get()->setRendererSurface(previous_renderer);
|
||||
}
|
||||
|
||||
// Actualiza la animación de la consola
|
||||
void Console::update(float delta_time) { // NOLINT(readability-function-cognitive-complexity)
|
||||
if (status_ == Status::HIDDEN) {
|
||||
// Parpadeig del cursor (només quan ACTIVE)
|
||||
void Console::updateCursorBlink(float delta_time) {
|
||||
cursor_timer_ += delta_time;
|
||||
const float THRESHOLD = cursor_visible_ ? CURSOR_ON_TIME : CURSOR_OFF_TIME;
|
||||
if (cursor_timer_ >= THRESHOLD) {
|
||||
cursor_timer_ = 0.0F;
|
||||
cursor_visible_ = !cursor_visible_;
|
||||
}
|
||||
}
|
||||
|
||||
// Revelat lletra a lletra de msg_lines_ (només quan ACTIVE)
|
||||
void Console::updateTypewriter(float delta_time) {
|
||||
const int TOTAL_CHARS = std::accumulate(msg_lines_.begin(), msg_lines_.end(), 0, [](int acc, const auto& line) { return acc + static_cast<int>(line.size()); });
|
||||
if (typewriter_chars_ >= TOTAL_CHARS) { return; }
|
||||
typewriter_timer_ += delta_time;
|
||||
while (typewriter_timer_ >= TYPEWRITER_CHAR_DELAY && typewriter_chars_ < TOTAL_CHARS) {
|
||||
typewriter_timer_ -= TYPEWRITER_CHAR_DELAY;
|
||||
++typewriter_chars_;
|
||||
}
|
||||
}
|
||||
|
||||
// Animació d'altura quan msg_lines_ canvia (només quan ACTIVE i height_ != target_height_)
|
||||
void Console::updateResizeAnimation(float delta_time) {
|
||||
if (anim_progress_ == 0.0F) {
|
||||
// Iniciar animació de resize
|
||||
anim_start_ = height_;
|
||||
anim_end_ = target_height_;
|
||||
}
|
||||
anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F);
|
||||
const float PREV_HEIGHT = height_;
|
||||
height_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_));
|
||||
if (anim_progress_ >= 1.0F) {
|
||||
height_ = target_height_;
|
||||
anim_progress_ = 0.0F;
|
||||
}
|
||||
// Actualitzar el Notifier incrementalment amb el delta d'altura
|
||||
if (Notifier::get() != nullptr) {
|
||||
const int DELTA_PX = static_cast<int>(height_) - static_cast<int>(PREV_HEIGHT);
|
||||
if (DELTA_PX > 0) {
|
||||
Notifier::get()->addYOffset(DELTA_PX);
|
||||
notifier_offset_applied_ += DELTA_PX;
|
||||
} else if (DELTA_PX < 0) {
|
||||
Notifier::get()->removeYOffset(-DELTA_PX);
|
||||
notifier_offset_applied_ += DELTA_PX;
|
||||
}
|
||||
}
|
||||
// Reconstruir la Surface al nou tamany (xicoteta: 256×~18-72px)
|
||||
const float WIDTH = Options::game.width;
|
||||
surface_ = std::make_shared<Surface>(WIDTH, height_);
|
||||
sprite_->setSurface(surface_);
|
||||
}
|
||||
|
||||
// Animació RISING/VANISHING (basada en temps amb easing)
|
||||
void Console::updateOpenCloseAnimation(float delta_time) {
|
||||
anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F);
|
||||
y_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_));
|
||||
|
||||
if (anim_progress_ < 1.0F) { return; }
|
||||
y_ = anim_end_;
|
||||
anim_progress_ = 0.0F;
|
||||
if (status_ == Status::RISING) {
|
||||
status_ = Status::ACTIVE;
|
||||
return;
|
||||
}
|
||||
status_ = Status::HIDDEN;
|
||||
// Reset del missatge una vegada completament oculta
|
||||
msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)};
|
||||
target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
|
||||
}
|
||||
|
||||
// Actualiza la animación de la consola
|
||||
void Console::update(float delta_time) {
|
||||
if (status_ == Status::HIDDEN) { return; }
|
||||
|
||||
// Parpadeo del cursor (solo cuando activa)
|
||||
if (status_ == Status::ACTIVE) {
|
||||
cursor_timer_ += delta_time;
|
||||
const float THRESHOLD = cursor_visible_ ? CURSOR_ON_TIME : CURSOR_OFF_TIME;
|
||||
if (cursor_timer_ >= THRESHOLD) {
|
||||
cursor_timer_ = 0.0F;
|
||||
cursor_visible_ = !cursor_visible_;
|
||||
updateCursorBlink(delta_time);
|
||||
updateTypewriter(delta_time);
|
||||
if (height_ != target_height_) {
|
||||
updateResizeAnimation(delta_time);
|
||||
}
|
||||
}
|
||||
|
||||
// Efecto typewriter: revelar letras una a una (solo cuando ACTIVE)
|
||||
if (status_ == Status::ACTIVE) {
|
||||
const int TOTAL_CHARS = std::accumulate(msg_lines_.begin(), msg_lines_.end(), 0, [](int acc, const auto& line) { return acc + static_cast<int>(line.size()); });
|
||||
if (typewriter_chars_ < TOTAL_CHARS) {
|
||||
typewriter_timer_ += delta_time;
|
||||
while (typewriter_timer_ >= TYPEWRITER_CHAR_DELAY && typewriter_chars_ < TOTAL_CHARS) {
|
||||
typewriter_timer_ -= TYPEWRITER_CHAR_DELAY;
|
||||
++typewriter_chars_;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Animación de altura (resize cuando msg_lines_ cambia); solo en ACTIVE
|
||||
if (status_ == Status::ACTIVE && height_ != target_height_) {
|
||||
if (anim_progress_ == 0.0F) {
|
||||
// Iniciar animación de resize
|
||||
anim_start_ = height_;
|
||||
anim_end_ = target_height_;
|
||||
}
|
||||
anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F);
|
||||
const float PREV_HEIGHT = height_;
|
||||
height_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_));
|
||||
if (anim_progress_ >= 1.0F) {
|
||||
height_ = target_height_;
|
||||
anim_progress_ = 0.0F;
|
||||
}
|
||||
// Actualizar el Notifier incrementalmente con el delta de altura
|
||||
if (Notifier::get() != nullptr) {
|
||||
const int DELTA_PX = static_cast<int>(height_) - static_cast<int>(PREV_HEIGHT);
|
||||
if (DELTA_PX > 0) {
|
||||
Notifier::get()->addYOffset(DELTA_PX);
|
||||
notifier_offset_applied_ += DELTA_PX;
|
||||
} else if (DELTA_PX < 0) {
|
||||
Notifier::get()->removeYOffset(-DELTA_PX);
|
||||
notifier_offset_applied_ += DELTA_PX;
|
||||
}
|
||||
}
|
||||
// Reconstruir la Surface al nuevo tamaño (pequeña: 256×~18-72px)
|
||||
const float WIDTH = Options::game.width;
|
||||
surface_ = std::make_shared<Surface>(WIDTH, height_);
|
||||
sprite_->setSurface(surface_);
|
||||
}
|
||||
|
||||
// Redibujar texto cada frame
|
||||
redrawText();
|
||||
|
||||
// Animación de apertura/cierre (basada en tiempo)
|
||||
if (status_ == Status::RISING || status_ == Status::VANISHING) {
|
||||
anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F);
|
||||
y_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_));
|
||||
|
||||
if (anim_progress_ >= 1.0F) {
|
||||
y_ = anim_end_;
|
||||
anim_progress_ = 0.0F;
|
||||
if (status_ == Status::RISING) {
|
||||
status_ = Status::ACTIVE;
|
||||
} else {
|
||||
status_ = Status::HIDDEN;
|
||||
msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)};
|
||||
target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
|
||||
}
|
||||
}
|
||||
updateOpenCloseAnimation(delta_time);
|
||||
}
|
||||
|
||||
SDL_FRect rect = {.x = 0, .y = y_, .w = Options::game.width, .h = height_};
|
||||
@@ -334,94 +342,103 @@ void Console::toggle() {
|
||||
}
|
||||
}
|
||||
|
||||
// Insereix caràcters imprimibles a input_line_ (filtra control i la toggle key activa)
|
||||
void Console::handleTextInput(const SDL_Event& event) {
|
||||
if (static_cast<unsigned char>(event.text.text[0]) < 32) { return; }
|
||||
// Ignorar text si la tecla toggle està pulsada (evita escriure el seu caràcter)
|
||||
if (KeyConfig::get() != nullptr) {
|
||||
SDL_Keycode toggle_key = KeyConfig::get()->key("GLOBAL", "console");
|
||||
SDL_Scancode toggle_sc = SDL_GetScancodeFromKey(toggle_key, nullptr);
|
||||
if (toggle_sc != SDL_SCANCODE_UNKNOWN) {
|
||||
const bool* ks = SDL_GetKeyboardState(nullptr);
|
||||
if (ks[toggle_sc]) { return; }
|
||||
}
|
||||
}
|
||||
if (static_cast<int>(input_line_.size()) < MAX_LINE_CHARS) {
|
||||
input_line_ += event.text.text;
|
||||
}
|
||||
tab_matches_.clear();
|
||||
}
|
||||
|
||||
// Navega enrere a l'historial (cap a comandes més antigues)
|
||||
void Console::handleHistoryUp() {
|
||||
tab_matches_.clear();
|
||||
if (history_index_ >= static_cast<int>(history_.size()) - 1) { return; }
|
||||
if (history_index_ == -1) { saved_input_ = input_line_; }
|
||||
++history_index_;
|
||||
input_line_ = history_[static_cast<size_t>(history_index_)];
|
||||
}
|
||||
|
||||
// Navega cap al present a l'historial (cap a comandes més recents)
|
||||
void Console::handleHistoryDown() {
|
||||
tab_matches_.clear();
|
||||
if (history_index_ < 0) { return; }
|
||||
--history_index_;
|
||||
input_line_ = (history_index_ == -1) ? saved_input_ : history_[static_cast<size_t>(history_index_)];
|
||||
}
|
||||
|
||||
// Autocompletat per TAB: calcula candidats si cal i cicla
|
||||
void Console::handleTab() {
|
||||
if (tab_matches_.empty()) {
|
||||
std::string upper;
|
||||
for (const unsigned char C : input_line_) { upper += static_cast<char>(std::toupper(C)); }
|
||||
|
||||
const size_t SPACE_POS = upper.rfind(' ');
|
||||
if (SPACE_POS == std::string::npos) {
|
||||
// Mode comanda: cicla keywords visibles que comencen pel prefix
|
||||
const auto KEYWORDS = registry_.getVisibleKeywords();
|
||||
std::ranges::copy_if(KEYWORDS, std::back_inserter(tab_matches_), [&upper](const auto& kw) { return upper.empty() || kw.starts_with(upper); });
|
||||
} else {
|
||||
const std::string BASE_CMD = upper.substr(0, SPACE_POS);
|
||||
const std::string SUB_PREFIX = upper.substr(SPACE_POS + 1);
|
||||
const auto OPTS = registry_.getCompletions(BASE_CMD);
|
||||
for (const auto& arg : OPTS) {
|
||||
if (!SUB_PREFIX.empty() && !std::string_view{arg}.starts_with(SUB_PREFIX)) { continue; }
|
||||
std::string match = BASE_CMD;
|
||||
match += ' ';
|
||||
match += arg;
|
||||
tab_matches_.emplace_back(std::move(match));
|
||||
}
|
||||
}
|
||||
tab_index_ = -1;
|
||||
}
|
||||
if (tab_matches_.empty()) { return; }
|
||||
tab_index_ = (tab_index_ + 1) % static_cast<int>(tab_matches_.size());
|
||||
std::string result = tab_matches_[static_cast<size_t>(tab_index_)];
|
||||
std::ranges::transform(result, result.begin(), [](char c) { return static_cast<char>(std::tolower(static_cast<unsigned char>(c))); });
|
||||
input_line_ = result;
|
||||
}
|
||||
|
||||
// Procesa el evento SDL: entrada de texto, Backspace, Enter
|
||||
void Console::handleEvent(const SDL_Event& event) { // NOLINT(readability-function-cognitive-complexity)
|
||||
void Console::handleEvent(const SDL_Event& event) {
|
||||
if (status_ != Status::ACTIVE) { return; }
|
||||
|
||||
if (event.type == SDL_EVENT_TEXT_INPUT) {
|
||||
// Filtrar caracteres de control (tab, newline, etc.)
|
||||
if (static_cast<unsigned char>(event.text.text[0]) < 32) { return; }
|
||||
// Ignorar texto si la tecla toggle está pulsada (evita escribir su carácter)
|
||||
if (KeyConfig::get() != nullptr) {
|
||||
SDL_Keycode toggle_key = KeyConfig::get()->key("GLOBAL", "console");
|
||||
SDL_Scancode toggle_sc = SDL_GetScancodeFromKey(toggle_key, nullptr);
|
||||
if (toggle_sc != SDL_SCANCODE_UNKNOWN) {
|
||||
const bool* ks = SDL_GetKeyboardState(nullptr);
|
||||
if (ks[toggle_sc]) { return; }
|
||||
}
|
||||
}
|
||||
if (static_cast<int>(input_line_.size()) < MAX_LINE_CHARS) {
|
||||
input_line_ += event.text.text;
|
||||
}
|
||||
tab_matches_.clear();
|
||||
handleTextInput(event);
|
||||
return;
|
||||
}
|
||||
if (event.type != SDL_EVENT_KEY_DOWN) { return; }
|
||||
|
||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||
switch (event.key.scancode) {
|
||||
case SDL_SCANCODE_BACKSPACE:
|
||||
tab_matches_.clear();
|
||||
if (!input_line_.empty()) { input_line_.pop_back(); }
|
||||
break;
|
||||
case SDL_SCANCODE_RETURN:
|
||||
case SDL_SCANCODE_KP_ENTER:
|
||||
processCommand();
|
||||
break;
|
||||
case SDL_SCANCODE_UP:
|
||||
// Navegar hacia atrás en el historial
|
||||
tab_matches_.clear();
|
||||
if (history_index_ < static_cast<int>(history_.size()) - 1) {
|
||||
if (history_index_ == -1) { saved_input_ = input_line_; }
|
||||
++history_index_;
|
||||
input_line_ = history_[static_cast<size_t>(history_index_)];
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_DOWN:
|
||||
// Navegar hacia el presente en el historial
|
||||
tab_matches_.clear();
|
||||
if (history_index_ >= 0) {
|
||||
--history_index_;
|
||||
input_line_ = (history_index_ == -1)
|
||||
? saved_input_
|
||||
: history_[static_cast<size_t>(history_index_)];
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_TAB: {
|
||||
if (tab_matches_.empty()) {
|
||||
// Calcular el input actual en mayúsculas
|
||||
std::string upper;
|
||||
for (unsigned char c : input_line_) { upper += static_cast<char>(std::toupper(c)); }
|
||||
|
||||
const size_t SPACE_POS = upper.rfind(' ');
|
||||
if (SPACE_POS == std::string::npos) {
|
||||
// Modo comando: ciclar keywords visibles que empiecen por el prefijo
|
||||
const auto VISIBLE = registry_.getVisibleKeywords();
|
||||
std::ranges::copy_if(VISIBLE, std::back_inserter(tab_matches_), [&](const auto& kw) { return upper.empty() || kw.starts_with(upper); });
|
||||
} else {
|
||||
const std::string BASE_CMD = upper.substr(0, SPACE_POS);
|
||||
const std::string SUB_PREFIX = upper.substr(SPACE_POS + 1);
|
||||
const auto OPTS = registry_.getCompletions(BASE_CMD);
|
||||
for (const auto& arg : OPTS) {
|
||||
if (SUB_PREFIX.empty() || std::string_view{arg}.starts_with(SUB_PREFIX)) {
|
||||
std::string match = BASE_CMD;
|
||||
match += ' ';
|
||||
match += arg;
|
||||
tab_matches_.emplace_back(std::move(match));
|
||||
}
|
||||
}
|
||||
}
|
||||
tab_index_ = -1;
|
||||
}
|
||||
if (tab_matches_.empty()) { break; }
|
||||
tab_index_ = (tab_index_ + 1) % static_cast<int>(tab_matches_.size());
|
||||
std::string result = tab_matches_[static_cast<size_t>(tab_index_)];
|
||||
std::ranges::transform(result, result.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
input_line_ = result;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (event.key.scancode) {
|
||||
case SDL_SCANCODE_BACKSPACE:
|
||||
tab_matches_.clear();
|
||||
if (!input_line_.empty()) { input_line_.pop_back(); }
|
||||
break;
|
||||
case SDL_SCANCODE_RETURN:
|
||||
case SDL_SCANCODE_KP_ENTER:
|
||||
processCommand();
|
||||
break;
|
||||
case SDL_SCANCODE_UP:
|
||||
handleHistoryUp();
|
||||
break;
|
||||
case SDL_SCANCODE_DOWN:
|
||||
handleHistoryDown();
|
||||
break;
|
||||
case SDL_SCANCODE_TAB:
|
||||
handleTab();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,14 @@ class Console {
|
||||
void redrawText(); // Redibuja el texto dinámico (msg + input + cursor)
|
||||
void processCommand(); // Procesa el comando introducido por el usuario
|
||||
[[nodiscard]] auto wrapText(const std::string& text) const -> std::vector<std::string>; // Word-wrap por ancho en píxeles
|
||||
void updateCursorBlink(float delta_time); // Parpadeig del cursor (només quan ACTIVE)
|
||||
void updateTypewriter(float delta_time); // Revelat lletra a lletra de msg_lines_
|
||||
void updateResizeAnimation(float delta_time); // Animació d'altura quan msg_lines_ canvia
|
||||
void updateOpenCloseAnimation(float delta_time); // Animació RISING/VANISHING
|
||||
void handleTextInput(const SDL_Event& event); // Insereix caràcters imprimibles a input_line_
|
||||
void handleHistoryUp(); // Navega enrere a l'historial
|
||||
void handleHistoryDown(); // Navega cap al present a l'historial
|
||||
void handleTab(); // Autocompletat per TAB: calcula candidats si cal i cicla
|
||||
|
||||
// Objetos de renderizado
|
||||
std::shared_ptr<Text> text_;
|
||||
|
||||
+148
-153
@@ -69,8 +69,10 @@ static auto applyPreset(const std::vector<std::string>& args) -> std::string {
|
||||
if (COUNT == 0) { return "No " + SHADER_LABEL + " presets available"; }
|
||||
|
||||
const auto PRESET_NAME = [&]() -> std::string {
|
||||
const auto& name = IS_CRTPI ? presets_crtpi[static_cast<size_t>(current_idx)].name // NOLINT(clang-analyzer-core.CallAndMessage)
|
||||
: presets_postfx[static_cast<size_t>(current_idx)].name; // NOLINT(clang-analyzer-core.CallAndMessage)
|
||||
// NOLINTBEGIN(clang-analyzer-core.CallAndMessage) -- fals positiu: la guard de la línia 69 garanteix COUNT > 0, així que current_idx és sempre dins de rang.
|
||||
const auto& name = IS_CRTPI ? presets_crtpi[static_cast<size_t>(current_idx)].name
|
||||
: presets_postfx[static_cast<size_t>(current_idx)].name;
|
||||
// NOLINTEND(clang-analyzer-core.CallAndMessage)
|
||||
return prettyName(name);
|
||||
};
|
||||
|
||||
@@ -424,7 +426,7 @@ static auto cmdSound(const std::vector<std::string>& args) -> std::string {
|
||||
|
||||
#ifdef _DEBUG
|
||||
// DEBUG [MODE [ON|OFF]|START [HERE|ROOM|POS|SCENE <name>]]
|
||||
static auto cmdDebug(const std::vector<std::string>& args) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
static auto cmdDebug(const std::vector<std::string>& args) -> std::string {
|
||||
// --- START subcommands (START SCENE works from any scene) ---
|
||||
if (!args.empty() && args[0] == "START") {
|
||||
// START SCENE [<name>] — works from any scene
|
||||
@@ -523,7 +525,7 @@ static auto changeRoomWithEditor(const std::string& room_file) -> std::string {
|
||||
return std::string("Room: ") + room_file;
|
||||
}
|
||||
|
||||
static auto cmdRoom(const std::vector<std::string>& args) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
static auto cmdRoom(const std::vector<std::string>& args) -> std::string {
|
||||
if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
|
||||
if (args.empty()) { return "usage: room <num>|next|prev|left|right|up|down"; }
|
||||
|
||||
@@ -611,7 +613,7 @@ static auto cmdScene(const std::vector<std::string>& args) -> std::string {
|
||||
}
|
||||
|
||||
// EDIT [ON|OFF|REVERT]
|
||||
static auto cmdEdit(const std::vector<std::string>& args) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
static auto cmdEdit(const std::vector<std::string>& args) -> std::string {
|
||||
if (args.empty()) {
|
||||
// Toggle: si está activo → off, si no → on
|
||||
if ((MapEditor::get() != nullptr) && MapEditor::get()->isActive()) {
|
||||
@@ -812,47 +814,38 @@ static auto cmdHide(const std::vector<std::string>& args) -> std::string {
|
||||
}
|
||||
|
||||
// CHEAT [subcomando]
|
||||
static auto cmdCheat(const std::vector<std::string>& args) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
// Apply ON/OFF/toggle a un cheat binari. mode="" → toggle; "ON"/"OFF" → estableix; altra cosa → cadena buida (usage error).
|
||||
static auto applyCheatToggle(Options::Cheat::State& cheat, std::string_view mode, std::string_view label) -> std::string {
|
||||
using State = Options::Cheat::State;
|
||||
if (mode.empty()) {
|
||||
cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED;
|
||||
} else if (mode == "ON") {
|
||||
if (cheat == State::ENABLED) { return std::string(label) + " already ON"; }
|
||||
cheat = State::ENABLED;
|
||||
} else if (mode == "OFF") {
|
||||
if (cheat == State::DISABLED) { return std::string(label) + " already OFF"; }
|
||||
cheat = State::DISABLED;
|
||||
} else {
|
||||
return {}; // sentinel: mode invàlid
|
||||
}
|
||||
return std::string(label) + " " + (cheat == State::ENABLED ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
static auto cmdCheat(const std::vector<std::string>& args) -> std::string {
|
||||
if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
|
||||
if (args.empty()) { return "usage: cheat [infinite lives|invincibility]"; }
|
||||
|
||||
// CHEAT INFINITE LIVES [ON|OFF]
|
||||
if (args[0] == "INFINITE") {
|
||||
if (args.size() < 2 || args[1] != "LIVES") { return "usage: cheat infinite lives [on|off]"; }
|
||||
auto& cheat = Options::cheats.infinite_lives;
|
||||
using State = Options::Cheat::State;
|
||||
const std::vector<std::string> REST(args.begin() + 2, args.end());
|
||||
// cppcheck-suppress knownConditionTrueFalse -- cppcheck no infiere que REST puede estar vacío cuando args.size() == 2.
|
||||
if (REST.empty()) {
|
||||
cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED;
|
||||
} else if (REST[0] == "ON") {
|
||||
if (cheat == State::ENABLED) { return "Infinite lives already ON"; }
|
||||
cheat = State::ENABLED;
|
||||
} else if (REST[0] == "OFF") {
|
||||
if (cheat == State::DISABLED) { return "Infinite lives already OFF"; }
|
||||
cheat = State::DISABLED;
|
||||
} else {
|
||||
return "usage: cheat infinite lives [on|off]";
|
||||
}
|
||||
return std::string("Infinite lives ") + (cheat == State::ENABLED ? "ON" : "OFF");
|
||||
const std::string_view MODE = (args.size() > 2) ? std::string_view(args[2]) : std::string_view();
|
||||
const std::string RES = applyCheatToggle(Options::cheats.infinite_lives, MODE, "Infinite lives");
|
||||
return RES.empty() ? "usage: cheat infinite lives [on|off]" : RES;
|
||||
}
|
||||
|
||||
// CHEAT INVINCIBILITY [ON|OFF]
|
||||
if (args[0] == "INVINCIBILITY" || args[0] == "INVENCIBILITY") {
|
||||
auto& cheat = Options::cheats.invincible;
|
||||
using State = Options::Cheat::State;
|
||||
if (args.size() == 1) {
|
||||
cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED;
|
||||
} else if (args[1] == "ON") {
|
||||
if (cheat == State::ENABLED) { return "Invincibility already ON"; }
|
||||
cheat = State::ENABLED;
|
||||
} else if (args[1] == "OFF") {
|
||||
if (cheat == State::DISABLED) { return "Invincibility already OFF"; }
|
||||
cheat = State::DISABLED;
|
||||
} else {
|
||||
return "usage: cheat invincibility [on|off]";
|
||||
}
|
||||
return std::string("Invincibility ") + (cheat == State::ENABLED ? "ON" : "OFF");
|
||||
const std::string_view MODE = (args.size() > 1) ? std::string_view(args[1]) : std::string_view();
|
||||
const std::string RES = applyCheatToggle(Options::cheats.invincible, MODE, "Invincibility");
|
||||
return RES.empty() ? "usage: cheat invincibility [on|off]" : RES;
|
||||
}
|
||||
|
||||
return "usage: cheat [infinite lives|invincibility]";
|
||||
@@ -908,7 +901,7 @@ static auto cmdSize(const std::vector<std::string>& /*unused*/) -> std::string {
|
||||
}
|
||||
|
||||
// CONSOLE [TRANSPARENT [ON|OFF]|BG|MSG|PROMPT|COMMAND <0-255>]
|
||||
static auto cmdConsole(const std::vector<std::string>& args) -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
static auto cmdConsole(const std::vector<std::string>& args) -> std::string {
|
||||
if (args.empty()) {
|
||||
return std::string("Console ") + (Options::console.transparent ? "transparent" : "solid") + " bg:" + std::to_string(Options::console.bg_color) + " msg:" + std::to_string(Options::console.msg_color) + " prompt:" + std::to_string(Options::console.prompt_color) + " cmd:" + std::to_string(Options::console.command_color);
|
||||
}
|
||||
@@ -953,7 +946,7 @@ static auto cmdConsole(const std::vector<std::string>& args) -> std::string { /
|
||||
|
||||
// ── CommandRegistry ──────────────────────────────────────────────────────────
|
||||
|
||||
void CommandRegistry::registerHandlers() { // NOLINT(readability-function-cognitive-complexity)
|
||||
void CommandRegistry::registerHandlers() {
|
||||
handlers_["cmd_shader"] = cmdShader;
|
||||
handlers_["cmd_border"] = cmdBorder;
|
||||
handlers_["cmd_fullscreen"] = cmdFullscreen;
|
||||
@@ -1072,7 +1065,108 @@ void CommandRegistry::registerHandlers() { // NOLINT(readability-function-cogni
|
||||
#endif
|
||||
}
|
||||
|
||||
void CommandRegistry::load(const std::string& yaml_path) { // NOLINT(readability-function-cognitive-complexity)
|
||||
namespace {
|
||||
// Parseja un node "scope" (string o sequence) a un vector. Buit si node és buit.
|
||||
auto parseScopeNode(const fkyaml::node& scope_node) -> std::vector<std::string> {
|
||||
std::vector<std::string> result;
|
||||
if (scope_node.is_sequence()) {
|
||||
std::ranges::transform(scope_node, std::back_inserter(result), [](const auto& s) { return s.template get_value<std::string>(); });
|
||||
} else {
|
||||
result.push_back(scope_node.get_value<std::string>());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parseja un mapping de path → [options] en l'unordered_map de destí
|
||||
void parseCompletionsNode(const fkyaml::node& completions_node, std::unordered_map<std::string, std::vector<std::string>>& out) {
|
||||
for (auto it = completions_node.begin(); it != completions_node.end(); ++it) {
|
||||
auto path = it.key().get_value<std::string>();
|
||||
std::vector<std::string> opts;
|
||||
std::ranges::transform(*it, std::back_inserter(opts), [](const auto& opt) { return opt.template get_value<std::string>(); });
|
||||
out[path] = std::move(opts);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
// Aplica camps "debug_extras" sobre un CommandDef ja inicialitzat (només en _DEBUG)
|
||||
void applyDebugExtras(const fkyaml::node& extras, CommandDef& def) {
|
||||
if (extras.contains("description")) { def.description = extras["description"].get_value<std::string>(); }
|
||||
if (extras.contains("usage")) { def.usage = extras["usage"].get_value<std::string>(); }
|
||||
if (extras.contains("hidden")) { def.hidden = extras["hidden"].get_value<bool>(); }
|
||||
if (extras.contains("help_hidden")) { def.help_hidden = extras["help_hidden"].get_value<bool>(); }
|
||||
if (extras.contains("completions")) {
|
||||
def.completions.clear();
|
||||
parseCompletionsNode(extras["completions"], def.completions);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Parseja un cmd_node a CommandDef. Hereta de la categoria si el comand no defineix scope/debug_only.
|
||||
auto parseCommandDef(const fkyaml::node& cmd_node, const std::string& category, bool cat_debug_only, const std::vector<std::string>& cat_scopes) -> CommandDef {
|
||||
CommandDef def;
|
||||
def.keyword = cmd_node["keyword"].get_value<std::string>();
|
||||
def.handler_id = cmd_node["handler"].get_value<std::string>();
|
||||
def.category = category;
|
||||
def.description = cmd_node.contains("description") ? cmd_node["description"].get_value<std::string>() : "";
|
||||
def.usage = cmd_node.contains("usage") ? cmd_node["usage"].get_value<std::string>() : def.keyword;
|
||||
def.instant = cmd_node.contains("instant") && cmd_node["instant"].get_value<bool>();
|
||||
def.hidden = cmd_node.contains("hidden") && cmd_node["hidden"].get_value<bool>();
|
||||
def.debug_only = cat_debug_only || (cmd_node.contains("debug_only") && cmd_node["debug_only"].get_value<bool>());
|
||||
def.help_hidden = cmd_node.contains("help_hidden") && cmd_node["help_hidden"].get_value<bool>();
|
||||
def.dynamic_completions = cmd_node.contains("dynamic_completions") && cmd_node["dynamic_completions"].get_value<bool>();
|
||||
|
||||
if (cmd_node.contains("scope")) {
|
||||
def.scopes = parseScopeNode(cmd_node["scope"]);
|
||||
} else if (!cat_scopes.empty()) {
|
||||
def.scopes = cat_scopes;
|
||||
} else {
|
||||
def.scopes.emplace_back("global");
|
||||
}
|
||||
|
||||
if (cmd_node.contains("completions")) {
|
||||
parseCompletionsNode(cmd_node["completions"], def.completions);
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
if (cmd_node.contains("debug_extras")) {
|
||||
applyDebugExtras(cmd_node["debug_extras"], def);
|
||||
}
|
||||
#endif
|
||||
return def;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Implementació del handler "cmd_help"
|
||||
auto CommandRegistry::buildHelp(const std::vector<std::string>& args) const -> std::string {
|
||||
if (!args.empty()) {
|
||||
// HELP KEYS [scope]: referencia de atajos de teclado
|
||||
if (args[0] == "KEYS") {
|
||||
return generateKeysHelp(args.size() > 1 ? args[1] : "");
|
||||
}
|
||||
// HELP <command>: mostrar ayuda detallada de un comando
|
||||
const auto* cmd = findCommand(args[0]);
|
||||
if (cmd == nullptr) { return "Unknown command: " + args[0]; }
|
||||
std::string kw_lower = cmd->keyword;
|
||||
std::ranges::transform(kw_lower, kw_lower.begin(), ::tolower);
|
||||
std::string result = kw_lower + ": " + cmd->description + "\n" + cmd->usage;
|
||||
|
||||
// Listar subcomandos/opciones si hay completions
|
||||
const auto OPTS = getCompletions(cmd->keyword);
|
||||
if (!OPTS.empty()) {
|
||||
result += "\noptions:";
|
||||
for (const auto& opt : OPTS) {
|
||||
std::string opt_lower = opt;
|
||||
std::ranges::transform(opt_lower, opt_lower.begin(), ::tolower);
|
||||
result += " " + opt_lower;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
std::cout << generateTerminalHelp();
|
||||
return generateConsoleHelp();
|
||||
}
|
||||
|
||||
void CommandRegistry::load(const std::string& yaml_path) {
|
||||
registerHandlers();
|
||||
|
||||
// Cargar y parsear el YAML
|
||||
@@ -1096,121 +1190,21 @@ void CommandRegistry::load(const std::string& yaml_path) { // NOLINT(readabilit
|
||||
for (const auto& cat_node : yaml["categories"]) {
|
||||
const auto CATEGORY = cat_node["name"].get_value<std::string>();
|
||||
const bool CAT_DEBUG_ONLY = cat_node.contains("debug_only") && cat_node["debug_only"].get_value<bool>();
|
||||
|
||||
// Scopes por defecto de la categoría
|
||||
std::vector<std::string> cat_scopes;
|
||||
if (cat_node.contains("scope")) {
|
||||
const auto& scope_node = cat_node["scope"];
|
||||
if (scope_node.is_sequence()) {
|
||||
std::ranges::transform(scope_node, std::back_inserter(cat_scopes), [](const auto& s) { return s.template get_value<std::string>(); });
|
||||
} else {
|
||||
cat_scopes.push_back(scope_node.get_value<std::string>());
|
||||
}
|
||||
}
|
||||
const std::vector<std::string> CAT_SCOPES = cat_node.contains("scope") ? parseScopeNode(cat_node["scope"]) : std::vector<std::string>{};
|
||||
|
||||
if (!cat_node.contains("commands")) { continue; }
|
||||
|
||||
for (const auto& cmd_node : cat_node["commands"]) {
|
||||
CommandDef def;
|
||||
def.keyword = cmd_node["keyword"].get_value<std::string>();
|
||||
def.handler_id = cmd_node["handler"].get_value<std::string>();
|
||||
def.category = CATEGORY;
|
||||
def.description = cmd_node.contains("description") ? cmd_node["description"].get_value<std::string>() : "";
|
||||
def.usage = cmd_node.contains("usage") ? cmd_node["usage"].get_value<std::string>() : def.keyword;
|
||||
def.instant = cmd_node.contains("instant") && cmd_node["instant"].get_value<bool>();
|
||||
def.hidden = cmd_node.contains("hidden") && cmd_node["hidden"].get_value<bool>();
|
||||
def.debug_only = CAT_DEBUG_ONLY || (cmd_node.contains("debug_only") && cmd_node["debug_only"].get_value<bool>());
|
||||
def.help_hidden = cmd_node.contains("help_hidden") && cmd_node["help_hidden"].get_value<bool>();
|
||||
def.dynamic_completions = cmd_node.contains("dynamic_completions") && cmd_node["dynamic_completions"].get_value<bool>();
|
||||
|
||||
// Scopes: del comando, o hereda de la categoría, o "global" por defecto
|
||||
if (cmd_node.contains("scope")) {
|
||||
const auto& scope_node = cmd_node["scope"];
|
||||
if (scope_node.is_sequence()) {
|
||||
std::ranges::transform(scope_node, std::back_inserter(def.scopes), [](const auto& s) { return s.template get_value<std::string>(); });
|
||||
} else {
|
||||
def.scopes.push_back(scope_node.get_value<std::string>());
|
||||
}
|
||||
} else if (!cat_scopes.empty()) {
|
||||
def.scopes = cat_scopes;
|
||||
} else {
|
||||
def.scopes.emplace_back("global");
|
||||
}
|
||||
|
||||
// Completions estáticas
|
||||
if (cmd_node.contains("completions")) {
|
||||
auto completions_node = cmd_node["completions"];
|
||||
for (auto it = completions_node.begin(); it != completions_node.end(); ++it) {
|
||||
auto path = it.key().get_value<std::string>();
|
||||
std::vector<std::string> opts;
|
||||
const auto& options_node = *it;
|
||||
std::ranges::transform(options_node, std::back_inserter(opts), [](const auto& opt) { return opt.template get_value<std::string>(); });
|
||||
def.completions[path] = std::move(opts);
|
||||
}
|
||||
}
|
||||
|
||||
// Aplicar debug_extras en debug builds
|
||||
#ifdef _DEBUG
|
||||
if (cmd_node.contains("debug_extras")) {
|
||||
const auto& extras = cmd_node["debug_extras"];
|
||||
if (extras.contains("description")) { def.description = extras["description"].get_value<std::string>(); }
|
||||
if (extras.contains("usage")) { def.usage = extras["usage"].get_value<std::string>(); }
|
||||
if (extras.contains("hidden")) { def.hidden = extras["hidden"].get_value<bool>(); }
|
||||
if (extras.contains("help_hidden")) { def.help_hidden = extras["help_hidden"].get_value<bool>(); }
|
||||
if (extras.contains("completions")) {
|
||||
def.completions.clear();
|
||||
auto extras_completions = extras["completions"];
|
||||
for (auto it = extras_completions.begin(); it != extras_completions.end(); ++it) {
|
||||
auto path = it.key().get_value<std::string>();
|
||||
std::vector<std::string> opts;
|
||||
const auto& options_node = *it;
|
||||
std::ranges::transform(options_node, std::back_inserter(opts), [](const auto& opt) { return opt.template get_value<std::string>(); });
|
||||
def.completions[path] = std::move(opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// En Release: saltar comandos debug_only
|
||||
CommandDef def = parseCommandDef(cmd_node, CATEGORY, CAT_DEBUG_ONLY, CAT_SCOPES);
|
||||
#ifndef _DEBUG
|
||||
if (def.debug_only) { continue; }
|
||||
#endif
|
||||
|
||||
commands_.push_back(std::move(def));
|
||||
}
|
||||
}
|
||||
|
||||
// Registrar el handler de HELP (captura this)
|
||||
handlers_["cmd_help"] = [this](const std::vector<std::string>& args) -> std::string {
|
||||
if (!args.empty()) {
|
||||
// HELP KEYS [scope]: referencia de atajos de teclado
|
||||
if (args[0] == "KEYS") {
|
||||
return generateKeysHelp(args.size() > 1 ? args[1] : "");
|
||||
}
|
||||
// HELP <command>: mostrar ayuda detallada de un comando
|
||||
const auto* cmd = findCommand(args[0]);
|
||||
if (cmd != nullptr) {
|
||||
std::string kw_lower = cmd->keyword;
|
||||
std::ranges::transform(kw_lower, kw_lower.begin(), ::tolower);
|
||||
std::string result = kw_lower + ": " + cmd->description + "\n" + cmd->usage;
|
||||
|
||||
// Listar subcomandos/opciones si hay completions
|
||||
auto opts = getCompletions(cmd->keyword);
|
||||
if (!opts.empty()) {
|
||||
result += "\noptions:";
|
||||
for (const auto& opt : opts) {
|
||||
std::string opt_lower = opt;
|
||||
std::ranges::transform(opt_lower, opt_lower.begin(), ::tolower);
|
||||
result += " " + opt_lower;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return "Unknown command: " + args[0];
|
||||
}
|
||||
std::cout << generateTerminalHelp();
|
||||
return generateConsoleHelp();
|
||||
};
|
||||
// Registrar el handler de HELP (delega a buildHelp per mantenir baixa la complexitat de load)
|
||||
handlers_["cmd_help"] = [this](const std::vector<std::string>& args) -> std::string { return buildHelp(args); };
|
||||
|
||||
// Aplanar completions en el mapa global
|
||||
for (const auto& cmd : commands_) {
|
||||
@@ -1298,12 +1292,17 @@ auto CommandRegistry::generateTerminalHelp() const -> std::string {
|
||||
return out.str();
|
||||
}
|
||||
|
||||
auto CommandRegistry::generateConsoleHelp() const -> std::string { // NOLINT(readability-function-cognitive-complexity)
|
||||
auto CommandRegistry::generateConsoleHelp() const -> std::string {
|
||||
// Agrupar comandos visibles por scope
|
||||
std::string global_cmds;
|
||||
std::string debug_cmds;
|
||||
std::string editor_cmds;
|
||||
|
||||
auto append_csv = [](std::string& dst, const std::string& token) {
|
||||
if (!dst.empty()) { dst += ", "; }
|
||||
dst += token;
|
||||
};
|
||||
|
||||
for (const auto& cmd : commands_) {
|
||||
if (cmd.help_hidden) { continue; }
|
||||
if (!isCommandVisible(cmd)) { continue; }
|
||||
@@ -1313,16 +1312,12 @@ auto CommandRegistry::generateConsoleHelp() const -> std::string { // NOLINT(re
|
||||
|
||||
// Clasificar por el PRIMER scope del comando
|
||||
const std::string& primary = cmd.scopes.empty() ? "global" : cmd.scopes[0];
|
||||
|
||||
if (primary == "editor") {
|
||||
if (!editor_cmds.empty()) { editor_cmds += ", "; }
|
||||
editor_cmds += kw_lower;
|
||||
append_csv(editor_cmds, kw_lower);
|
||||
} else if (primary == "debug") {
|
||||
if (!debug_cmds.empty()) { debug_cmds += ", "; }
|
||||
debug_cmds += kw_lower;
|
||||
append_csv(debug_cmds, kw_lower);
|
||||
} else {
|
||||
if (!global_cmds.empty()) { global_cmds += ", "; }
|
||||
global_cmds += kw_lower;
|
||||
append_csv(global_cmds, kw_lower);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,4 +59,5 @@ class CommandRegistry {
|
||||
void registerHandlers();
|
||||
[[nodiscard]] auto isCommandVisible(const CommandDef& cmd) const -> bool;
|
||||
[[nodiscard]] static auto generateKeysHelp(const std::string& scope_filter) -> std::string;
|
||||
[[nodiscard]] auto buildHelp(const std::vector<std::string>& args) const -> std::string; // Implementació del handler "cmd_help"
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@ class Color {
|
||||
* @param color Color del enum Cpc
|
||||
* @return Índice de paleta (Uint8)
|
||||
*/
|
||||
static constexpr auto getIndex(Cpc color) -> Uint8 { // NOLINT(readability-identifier-naming)
|
||||
static constexpr auto getIndex(Cpc color) -> Uint8 {
|
||||
return static_cast<Uint8>(color);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user