diff --git a/data/console/commands.yaml b/data/console/commands.yaml index 7233319..6b1461a 100644 --- a/data/console/commands.yaml +++ b/data/console/commands.yaml @@ -83,9 +83,9 @@ categories: - keyword: PALETTE handler: cmd_palette description: "Color palette (F5/F6)" - usage: "PALETTE [NEXT|PREV|SORT [ORIGINAL|LUMINANCE|SPECTRUM]|DEFAULT|]" + usage: "PALETTE [NEXT|PREV|SORT [ORIGINAL|OPTIMAL|REFERENCE]|DEFAULT|]" completions: - PALETTE SORT: [ORIGINAL, LUMINANCE, SPECTRUM] + PALETTE SORT: [ORIGINAL, OPTIMAL, REFERENCE] dynamic_completions: true - name: AUDIO diff --git a/data/room/02.yaml b/data/room/02.yaml index c159829..e176951 100644 --- a/data/room/02.yaml +++ b/data/room/02.yaml @@ -96,8 +96,8 @@ platforms: easing: cubicInOut frame: 0 path: - - {x: 5, y: 18, wait: 2} - - {x: 20, y: 18, wait: 2} - - {x: 20, y: 13, wait: 2} - - {x: 5, y: 13, wait: 2} + - {x: 5, y: 18, wait: 1} + - {x: 20, y: 18, wait: 1} + - {x: 20, y: 13, wait: 1} + - {x: 5, y: 13, wait: 1} diff --git a/source/core/rendering/palette_manager.cpp b/source/core/rendering/palette_manager.cpp index fc1bdc4..0778c42 100644 --- a/source/core/rendering/palette_manager.cpp +++ b/source/core/rendering/palette_manager.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include @@ -16,23 +16,23 @@ auto sortModeFromString(const std::string& str) -> PaletteSortMode { const std::string LOWER = toLower(str); - if (LOWER == "luminance") { return PaletteSortMode::LUMINANCE; } - if (LOWER == "spectrum") { return PaletteSortMode::SPECTRUM; } + if (LOWER == "optimal") { return PaletteSortMode::OPTIMAL; } + if (LOWER == "reference") { return PaletteSortMode::REFERENCE; } return PaletteSortMode::ORIGINAL; } auto sortModeToString(PaletteSortMode mode) -> std::string { switch (mode) { - case PaletteSortMode::LUMINANCE: - return "luminance"; - case PaletteSortMode::SPECTRUM: - return "spectrum"; + case PaletteSortMode::OPTIMAL: + return "optimal"; + case PaletteSortMode::REFERENCE: + return "reference"; default: return "original"; } } -// ── Paleta de referencia ZX Spectrum (16 colores ARGB) ─────────────────────── +// ── Helpers de color y ordenación de paletas ───────────────────────────────── namespace { // Helpers para extraer componentes RGB de un color ARGB (0xAARRGGBB) @@ -40,35 +40,6 @@ namespace { constexpr auto greenOf(Uint32 c) -> int { return static_cast((c >> 8) & 0xFF); } constexpr auto blueOf(Uint32 c) -> int { return static_cast(c & 0xFF); } - constexpr auto makeARGB(int r, int g, int b) -> Uint32 { - return (0xFFU << 24) | (static_cast(r) << 16) | (static_cast(g) << 8) | static_cast(b); - } - - // Paleta ZX Spectrum de referencia (misma que en tools/sort_palette/sort_palette.py) - constexpr std::array SPECTRUM_REFERENCE = { - makeARGB(0, 0, 0), - makeARGB(0, 0, 0), - makeARGB(0, 0, 216), - makeARGB(0, 0, 255), - makeARGB(216, 0, 0), - makeARGB(255, 0, 0), - makeARGB(216, 0, 216), - makeARGB(255, 0, 255), - makeARGB(0, 216, 0), - makeARGB(0, 255, 0), - makeARGB(0, 216, 216), - makeARGB(0, 255, 255), - makeARGB(216, 216, 0), - makeARGB(255, 255, 0), - makeARGB(216, 216, 216), - makeARGB(255, 255, 255), - }; - - // Luminancia percibida (ITU-R BT.709) - auto luminance(Uint32 color) -> double { - return (0.2126 * redOf(color)) + (0.7152 * greenOf(color)) + (0.0722 * blueOf(color)); - } - // Distancia euclídea al cuadrado en espacio RGB (no necesita sqrt para comparar) auto rgbDistanceSq(Uint32 a, Uint32 b) -> int { const int DR = redOf(a) - redOf(b); @@ -87,31 +58,118 @@ namespace { return count; } - // Ordenar por luminancia - auto sortByLuminance(const Palette& palette) -> Palette { - const size_t N = countActiveColors(palette); - std::vector colors(palette.begin(), palette.begin() + static_cast(N)); - std::ranges::sort(colors, [](Uint32 a, Uint32 b) { - return luminance(a) < luminance(b); - }); + // 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(countActiveColors(palette)); + const auto M = static_cast(countActiveColors(reference)); + const int SZ = std::max(N, M); + if (SZ == 0) { return palette; } - Palette result{}; - result.fill(0); - std::ranges::copy(colors, result.begin()); - return result; + // Matriz de coste NxM (ampliada a SZxSZ con ceros para hacerla cuadrada) + std::vector cost(SZ * 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 u(SZ + 1, 0); // Potenciales de filas + std::vector v(SZ + 1, 0); // Potenciales de columnas + std::vector p(SZ + 1, 0); // p[j] = fila asignada a columna j + std::vector way(SZ + 1, 0); + + for (int i = 1; i <= SZ; ++i) { + p[0] = i; + int j0 = 0; + std::vector minv(SZ + 1, INF); + std::vector 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 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; } - // Ordenar por similitud con la paleta ZX Spectrum (greedy matching) - auto sortBySpectrum(const Palette& palette) -> Palette { + // Asignación greedy de colores a la paleta de referencia. + // Más rápida pero puede asignar colores subóptimamente. + auto sortByReference(const Palette& palette, const Palette& reference) -> Palette { + const size_t REF_COUNT = countActiveColors(reference); const size_t N = countActiveColors(palette); std::vector available(palette.begin(), palette.begin() + static_cast(N)); std::vector result; result.reserve(N); - // Para cada color de referencia del Spectrum, buscar el más cercano disponible - const size_t REFS = std::min(N, SPECTRUM_REFERENCE.size()); + // Para cada color de referencia, buscar el más cercano disponible + const size_t REFS = std::min(N, REF_COUNT); for (size_t i = 0; i < REFS && !available.empty(); ++i) { - const Uint32 REF = SPECTRUM_REFERENCE[i]; + const Uint32 REF = reference[i]; auto best = std::ranges::min_element(available, [REF](Uint32 a, Uint32 b) { return rgbDistanceSq(a, REF) < rgbDistanceSq(b, REF); }); @@ -147,9 +205,18 @@ PaletteManager::PaletteManager( on_change_(std::move(on_change)) { current_ = findIndex(initial_name); - // Leer y aplicar paleta inicial directamente desde el archivo + // Leer la paleta de referencia directamente desde el archivo // (Resource::Cache aún no está disponible en este punto del ciclo de vida) - const auto INITIAL_PALETTE = sortPalette(readPalFile(palettes_.at(current_)), sort_mode_); + const std::string REF_NAME = std::string(Defaults::Video::PALETTE_NAME) + ".pal"; + for (const auto& p : palettes_) { + if (getFileName(p) == REF_NAME) { + reference_palette_ = readPalFile(p); + break; + } + } + + // Leer y aplicar paleta inicial + const auto INITIAL_PALETTE = sortPalette(readPalFile(palettes_.at(current_)), sort_mode_, reference_palette_); game_surface_->setPalette(INITIAL_PALETTE); border_surface_->setPalette(INITIAL_PALETTE); @@ -230,7 +297,7 @@ auto PaletteManager::getSortModeName() const -> std::string { void PaletteManager::apply() { Palette raw = Resource::Cache::get()->getPalette(palettes_.at(current_)); - Palette sorted = sortPalette(raw, sort_mode_); + Palette sorted = sortPalette(raw, sort_mode_, reference_palette_); game_surface_->loadPalette(sorted); border_surface_->loadPalette(sorted); @@ -264,12 +331,12 @@ void PaletteManager::processPathList() { } } -auto PaletteManager::sortPalette(const Palette& palette, PaletteSortMode mode) -> Palette { +auto PaletteManager::sortPalette(const Palette& palette, PaletteSortMode mode, const Palette& reference) -> Palette { switch (mode) { - case PaletteSortMode::LUMINANCE: - return sortByLuminance(palette); - case PaletteSortMode::SPECTRUM: - return sortBySpectrum(palette); + case PaletteSortMode::OPTIMAL: + return sortByOptimal(palette, reference); + case PaletteSortMode::REFERENCE: + return sortByReference(palette, reference); default: return palette; } diff --git a/source/core/rendering/palette_manager.hpp b/source/core/rendering/palette_manager.hpp index 7357eca..04623f1 100644 --- a/source/core/rendering/palette_manager.hpp +++ b/source/core/rendering/palette_manager.hpp @@ -15,9 +15,9 @@ class Surface; // Modo de ordenación de paletas enum class PaletteSortMode : int { - ORIGINAL = 0, // Paleta tal cual viene del fichero - LUMINANCE = 1, // Ordenada por luminancia percibida - SPECTRUM = 2, // Reordenada para imitar la paleta ZX Spectrum + ORIGINAL = 0, // Paleta tal cual viene del fichero + OPTIMAL = 1, // Asignación óptima a la paleta por defecto (Hungarian algorithm) + REFERENCE = 2, // Asignación greedy a la paleta por defecto COUNT = 3 }; @@ -53,9 +53,10 @@ class PaletteManager { void apply(); // Aplica la paleta actual a ambas surfaces [[nodiscard]] auto findIndex(const std::string& name) const -> size_t; // Localiza paleta por nombre en el vector void processPathList(); // Extrae nombres de archivo de las rutas completas - static auto sortPalette(const Palette& palette, PaletteSortMode mode) -> Palette; // Reordena una paleta según el modo + static auto sortPalette(const Palette& palette, PaletteSortMode mode, const Palette& reference) -> Palette; std::vector palettes_; + Palette reference_palette_{}; // Paleta de referencia para el modo REFERENCE size_t current_{0}; PaletteSortMode sort_mode_{PaletteSortMode::ORIGINAL}; std::shared_ptr game_surface_; diff --git a/source/game/options.hpp b/source/game/options.hpp index e202613..e841650 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -104,7 +104,7 @@ namespace Options { bool integer_scale{Defaults::Video::INTEGER_SCALE}; // Indica si el escalado de la imagen ha de ser entero en el modo a pantalla completa bool keep_aspect{Defaults::Video::KEEP_ASPECT}; // Indica si se ha de mantener la relación de aspecto al poner el modo a pantalla completa std::string palette{Defaults::Video::PALETTE_NAME}; // Paleta de colores a usar en el juego - std::string palette_sort{Defaults::Video::PALETTE_SORT}; // Modo de ordenación de la paleta (original/luminance/spectrum) + std::string palette_sort{Defaults::Video::PALETTE_SORT}; // Modo de ordenación de la paleta (original/optimal/reference) std::string info; // Información sobre el modo de vídeo Border border{}; // Borde de la pantalla GPU gpu{}; // Opciones de aceleración GPU diff --git a/source/game/ui/console_commands.cpp b/source/game/ui/console_commands.cpp index 73a1de2..1ea48e9 100644 --- a/source/game/ui/console_commands.cpp +++ b/source/game/ui/console_commands.cpp @@ -340,7 +340,7 @@ static auto cmdPalette(const std::vector& args) -> std::string { const auto PAL_NAME = []() -> std::string { return Screen::get()->getPalettePrettyName(); }; - if (args.empty()) { return "usage: palette [next|prev|sort [original|luminance|spectrum]|default|]"; } + if (args.empty()) { return "usage: palette [next|prev|sort [original|optimal|reference]|default|]"; } if (args[0] == "NEXT") { Screen::get()->nextPalette(); return "Palette: " + PAL_NAME(); @@ -360,12 +360,12 @@ static auto cmdPalette(const std::vector& args) -> std::string { } if (args[1] == "ORIGINAL") { Screen::get()->setPaletteSortMode(PaletteSortMode::ORIGINAL); - } else if (args[1] == "LUMINANCE") { - Screen::get()->setPaletteSortMode(PaletteSortMode::LUMINANCE); - } else if (args[1] == "SPECTRUM") { - Screen::get()->setPaletteSortMode(PaletteSortMode::SPECTRUM); + } else if (args[1] == "OPTIMAL") { + Screen::get()->setPaletteSortMode(PaletteSortMode::OPTIMAL); + } else if (args[1] == "REFERENCE") { + Screen::get()->setPaletteSortMode(PaletteSortMode::REFERENCE); } else { - return "Unknown sort mode. Use: original, luminance, spectrum"; + return "Unknown sort mode. Use: original, optimal, reference"; } return "Palette sort: " + Screen::get()->getPaletteSortModeName(); }