Compare commits

...

29 Commits

Author SHA1 Message Date
fc28586940 - faltava propietat mirror
- al fer clic es tanca la consola
2026-04-02 13:02:35 +02:00
273d9304dc editar propietats del enemic 2026-04-02 12:45:29 +02:00
3a5b16346b RoomSaver ja no reventa els yamals 2026-04-02 11:52:53 +02:00
0e61b94848 - es pot canviar d'habitacio amb el editor obert
- nous comandos per a navegar per les habitacions (left, right, up i down)
2026-04-02 11:38:46 +02:00
eca5a55d3c nova font per al editor 2026-04-02 11:30:16 +02:00
dccd0d41e4 tecla 9 per entrar i eixir del editor 2026-04-02 11:25:03 +02:00
20bac58814 al eixir del editor, recarrega la habitació nova 2026-04-02 11:21:08 +02:00
a6fae7b001 guardat dels canvis en la habitacio 2026-04-02 11:10:03 +02:00
b31346830f drag'n drop de enemics, boundaries i items 2026-04-02 10:53:21 +02:00
b6fec3eba7 drag'n drop del jugador 2026-04-02 10:39:57 +02:00
606388227c mostra les rutes dels enemics al editor 2026-04-02 10:36:41 +02:00
a2caf95005 començem a treballar en el editor 2026-04-02 10:28:19 +02:00
0bfb535d4d - actualització masiva de CLAUDE.md
- pujat a versió 1.13
2026-04-02 09:20:27 +02:00
405f2248ec actualitzat changelog 2026-04-02 08:57:24 +02:00
93b1cd80b7 corregit make release per a macos 2026-04-02 08:55:36 +02:00
b53bf87730 arreglos en makefile 2026-04-02 08:35:36 +02:00
015a9cc4e1 - restaurades les paletes amb la ordenacio original
- afegida opció de reordenar les paletes automaticament per luminositat o paregut a la paleta d'spectrum
2026-04-02 07:59:30 +02:00
3bd13b72cd afegit .vscode a git 2026-04-01 23:06:24 +02:00
c94adf39af reestructurat els comandos de consola 2026-04-01 22:46:02 +02:00
950eeffb07 el indicador de trucos ja no es el color del jugador sino que es veu al marcador 2026-04-01 22:12:52 +02:00
b37b62ef1e - pots canviar el color del jugador desde la consola (persistent)
- cokmprova que el color no siga el mateix que el del fono (canvia a default)
- eliminades animacions sobrants del jugador
- canviada la logica del marcador pero a mostrar la animació de les vides del jugador
- posibilitat d'utilitzar skins d'enemics en el jugador
- canvi en calent de la skin en el marcador (abans soles en el constructir)
2026-04-01 21:31:25 +02:00
0c8aa5fe50 fix: entrar i eixir al mode debug manté l'estat previ del jugador 2026-04-01 20:17:31 +02:00
fe520dd341 - Es pot posar shader preset directament per nom desde la consola
- shader preset i palette ja autocompleten amb la llista de noms
2026-04-01 20:08:55 +02:00
ec9a9aff81 Console ara llig els comandos desde un fitxer extern 2026-04-01 19:31:09 +02:00
f9c1c4843d reestructurat el apartat de video de config.yaml 2026-04-01 18:57:32 +02:00
a804ad1368 - posibilitat de desactivar la aceleració hardware desde el fitxer de configuració de manera mes intuitiva
- si no hi ha aceleració ja no va cap tecla ni comando relacionat amb els shaders
2026-04-01 18:24:22 +02:00
c689507982 - afegides noves paletes
- ordenades les paletes que tenien els color mal ubicats
- eliminades responsabilitats a Options sobre les paletes
- "pretty" name per a les paletes (canvia els "-" per " ")
- nova tool/ en python per a reordenar paletes
2026-03-31 20:02:18 +02:00
417643018f optimitzacions en Surface 2026-03-31 14:56:39 +02:00
2ed7316948 afegit changelog.md 2026-03-31 07:56:09 +02:00
82 changed files with 4523 additions and 1701 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
.cache/ .cache/
.vscode/
*data/config/config.yaml *data/config/config.yaml
*stats.txt *stats.txt
*.DS_Store *.DS_Store

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json"
}

170
CHANGELOG.md Normal file
View File

@@ -0,0 +1,170 @@
# Changelog
All notable changes to JailDoctor's Dilemma are documented here.
---
## [v1.12] - 2026-04-02
### Novedades
- **Color del jugador configurable:** se puede cambiar desde la consola (persistente), con comprobación automática para evitar que coincida con el color de fondo
- **Skins de enemigos para el jugador:** posibilidad de usar skins de enemigos en el jugador, con cambio en caliente en el marcador
- **Indicador de trucos mejorado:** ya no usa el color del jugador, ahora se muestra en el marcador
- **Shader presets por nombre:** se puede establecer un shader preset directamente por nombre desde la consola, con autocompletado
- **Comandos externos en consola:** la consola lee los comandos desde un fichero externo
- **Gestión de paletas mejorada:**
- Nuevas paletas añadidas
- Restaurado el orden original de las paletas
- Opción de reordenar paletas automáticamente por luminosidad o parecido a la paleta de Spectrum
- Nombres "pretty" para las paletas (cambia los `-` por ` `)
- Eliminadas responsabilidades de `Options` sobre las paletas
- Nueva herramienta en Python para reordenar paletas
- **Aceleración hardware configurable:** posibilidad de desactivar la aceleración hardware desde el fichero de configuración; si no hay aceleración, se deshabilitan teclas y comandos de shaders
- **Autocompletado mejorado:** shader preset y palette autocompletan con la lista de nombres
- Reestructuración de comandos de consola
- Reestructuración del apartado de vídeo en `config.yaml`
- Optimizaciones en `Surface`
### Correcciones
- Fix: entrar y salir del modo debug mantiene el estado previo del jugador
- Corregido Makefile: migración completa a cmake, detección automática de SO para release
---
## [v1.11] - 2026-03-31
### Novedades
- **PaletteManager:** refactorización de `Screen`, responsabilidades de gestión de paletas extraídas a clase propia
- **Consola 2.1:** la consola puede cambiar de paleta por nombre (`Screen` devuelve lista de paletas)
- **Zoom configurable:** `Screen` permite establecer el nivel de zoom directamente desde consola
- **Autocompletar en consola:** autocompletado de comandos con Tab (incluyendo soporte para armadura de lagarto)
- Generación automática de tabla de tab-completions en la consola
### Correcciones
- Fix: al entrar a GAME con la consola abierta, el jugador no tenía los inputs deshabilitados
- Fix: al hacer restart con la música del attract mode sonando, la música no paraba al ir al logo
- Fix: en modo debug, protección para que el jugador no caiga infinitamente si sale de pantalla
- Corregido el case en algunas respuestas de la consola
- Corregido Makefile
---
## [v1.10] - 2026-03-30
### Novedades
- **Consola 2.0:** rediseño completo de la consola de desarrollador
- Efecto typewriter al mostrar texto
- Separación de líneas automática
- Cambio de skin
- Historial y navegación mejorada
- Comandos para cheats, control de escena, debug, audio y shaders
- Teclas de función operativas con la consola abierta
- Límite de caracteres ampliado
- La consola ya no pausa al jugador
- Reorganización del sistema de comandos y aliases (`show info`, `hide info`, etc.)
- **RenderInfo:** nueva clase con animación para mostrar info de renderizado en pantalla
- **Soporte multi-shader:** comandos y teclas para manejar el nuevo diseño de shaders (SPIRV/SPIR-V)
- **Modo kiosko:** defaults y restricciones de comandos para modo kiosk
- **Supersampling Lanczos:** implementación de escalado Lanczos en el supersampling
- **Driver GPU configurable:** permite elegir driver de GPU o ninguno desde consola
- Cheats accesibles desde la consola
- Cambio y reinicio de escena desde la consola
- Posición e habitación inicial de debug configurables desde consola y fichero
- `Debug` carga posición e habitación inicial desde fichero
- Comandos de audio configurables desde consola
- Renderizado del dispositivo GPU en info_debug
- `Screen` optimizado (`textureToRenderer()`)
- Eliminado soporte para argumentos de línea de comandos
- Eliminado `Options::console`
- Help de consola organizado
### Correcciones
- Fix: vsync off no funcionaba en Wayland
- Fix: en TITLE, la consola no bloqueaba la pulsación del 1 al 4 y entraba a opciones
- Fix: dos logs de consola con formato incorrecto
- Fix: lógica para abrir y entrar a la jail (ahora usa número de habitación, no nombre)
- Corregido `compile_spirv.cmake` y la `system_folder` para shaders
- Corregido carácter de caret que se había perdido
- Eliminados acentos en títulos de habitaciones que causaban problemas con fuentes
- Revisadas y corregidas traducciones
- Corregidos ficheros `.fnt`
- Corrección en `Screen` para `std::setprecision()` (faltaba `#include <iomanip>`)
---
## [v1.09] - 2025-03-01
### Novedades
- **Refactorización a singletons:** `Screen`, `Input`, `Audio`, `Resource::Cache`, `Resource::List`, `Director`, `Cheevos`, `Debug` convertidos a singletons thread-safe
- **Smart pointers:** uso de `std::shared_ptr` y `std::unique_ptr` para gestión de recursos y sprites
- **Surfaces 8-bit indexadas:** nuevo sistema de renderizado con color indexado y paletas intercambiables
- **Sistema de notificaciones rediseñado:** nuevo engine de notificaciones con control de offset
- **Modos de vídeo mejorados:** la ventana mantiene posición al cambiar tamaño o activar borde; puede crecer según el escritorio
- **ItemTracker:** nuevo singleton para rastrear ítems recogidos
- **globalEvents:** nuevo sistema de eventos globales SDL
- **Barra de progreso en carga de recursos** (actualización cada 5 ítems para mayor rendimiento con vsync)
- **Métodos show/hide ventana:** métodos para mostrar u ocultar la ventana
- Afinada la clase `Options`
- Actualizada a la última versión de `jail_audio`
- Implementados shaders
- Nueva tipografía añadida
- Parametros de ficheros `.ani` migrados a snake_case
- Música de Title y attract mode restaurada
- Eliminado sistema online completo
### Correcciones
- Fix: notificaciones ya no ensucian la pantalla de carga
- Fix: no pintaba el efecto de carga del borde en `LoadingScreen`
- Fix: bug con el puntero a `ScoreboardData`
- Fix: carga de opciones y recursos corregida
- Eliminados acentos problemáticos
---
## [v1.08] - 2024-02-22
### Novedades
- Posibilidad de saltar la pantalla de carga ya completada desde el menú de título
- El `gamestate_title` puede empezar en diferentes estados
- Pantalla de carga con fade de paleta
- GIF loader: dibujado correcto de GIFs en pantalla
- Añadida `paleta.cpp`/`.h` y `gif.c`
### Correcciones
- Corregido bug en el fade de paleta (el canal azul no se propagaba)
- Arreglada la separación entre el título y el fade
- Online deshabilitado por defecto al crear el fichero de configuración
- Tiempo de la pantalla de carga aumentado
---
## [v1.07] - 2022-12-02
### Novedades
- El nombre de la habitación se pinta a partir de una textura
- Añadido Batman a FEEL THE HEAT
- Cielo de la Jail actualizado
- Retocada la pantalla de título
- Sprite de PACO modificado
- Nombre del enemigo diskette cambiado a floppy
- Cambios cosméticos en algunas habitaciones (BE CAREFUL WITH THE FUSE renombrado)
- El color de fondo de la habitación se pinta en la textura del mapa
- Optimizaciones en intro y title
- Preparación para compatibilidad con consolas
- Actualizado `jail_audio` a la última versión
- Eliminados la mayor parte de accesos a `vector::at()`
### Correcciones
- Corregido bug: en la jail se rellenaban las vidas mientras estaba activa la pausa
- Corregido memory leak en `texture.cpp`
- Corregido bug en apertura de la Jail
---
## [v1.0] - 2022-11-13
Versión de lanzamiento inicial.
---
*El formato de este changelog sigue [Keep a Changelog](https://keepachangelog.com/).*

154
CLAUDE.md
View File

@@ -53,7 +53,7 @@ cmake --build build --clean-first
./jaildoctors_dilemma ./jaildoctors_dilemma
``` ```
**Important:** The build directory is `/Users/sergio/Gitea/jaildoctors_dilemma/build` and the output executable is placed in the project root directory. **Important:** The build directory is `build/` relative to the project root and the output executable is placed in the project root directory.
### Testing in Headless Environment (SSH/Remote Server) ### Testing in Headless Environment (SSH/Remote Server)
@@ -67,17 +67,12 @@ sudo apt-get install xvfb
#### Running the Game in Headless Mode #### Running the Game in Headless Mode
**Option 1: Using the wrapper script (RECOMMENDED)** **Option 1: Using xvfb-run directly (RECOMMENDED)**
```bash
./run_headless.sh
```
**Option 2: Using xvfb-run directly**
```bash ```bash
xvfb-run -a ./jaildoctors_dilemma xvfb-run -a ./jaildoctors_dilemma
``` ```
**Option 3: Custom display configuration** **Option 2: Custom display configuration**
```bash ```bash
xvfb-run -a -s "-screen 0 1280x720x24" ./jaildoctors_dilemma xvfb-run -a -s "-screen 0 1280x720x24" ./jaildoctors_dilemma
``` ```
@@ -238,26 +233,39 @@ The architecture follows a **layered, modular design** with clear separation of
source/ source/
├── core/ # Core engine systems ├── core/ # Core engine systems
│ ├── audio/ # Audio management │ ├── audio/ # Audio management
│ │ ── audio.hpp/cpp # Audio singleton (music, sounds, volumes) │ │ ── audio.hpp/cpp # Audio singleton (music, sounds, volumes)
│ │ └── jail_audio.hpp # Custom jail_audio library wrapper
│ ├── input/ # Input handling │ ├── input/ # Input handling
│ │ ├── input.hpp/cpp # Input manager (keyboard, gamepad) │ │ ├── input.hpp/cpp # Input manager (keyboard, gamepad)
│ │ ├── input_types.hpp # Input type definitions
│ │ ├── global_inputs.hpp # Global input state │ │ ├── global_inputs.hpp # Global input state
│ │ └── mouse.hpp # Mouse input │ │ └── mouse.hpp # Mouse input
│ ├── locale/ # Localization
│ │ └── locale.hpp # Locale/language support
│ ├── rendering/ # Graphics rendering │ ├── rendering/ # Graphics rendering
│ │ ├── screen.hpp/cpp # Screen/window singleton, SDL renderer │ │ ├── screen.hpp/cpp # Screen/window singleton, SDL renderer
│ │ ├── surface.hpp/cpp # 8-bit indexed color surface abstraction │ │ ├── surface.hpp/cpp # 8-bit indexed color surface abstraction
│ │ ├── surface_sprite.hpp # Static sprite rendering │ │ ├── sprite/ # Sprite rendering classes
│ │ ├── surface_animated_sprite.hpp # Animated sprite with frame data │ │ ├── sprite.hpp # Static sprite rendering
│ │ ├── surface_moving_sprite.hpp # Moving sprite with velocity │ │ │ ├── animated_sprite.hpp # Animated sprite with frame data
│ │ ├── texture.hpp/cpp # SDL texture wrapper │ │ │ ├── moving_sprite.hpp # Moving sprite with velocity
│ │ │ └── dissolve_sprite.hpp # Dissolve transition sprite
│ │ ├── text.hpp/cpp # Text rendering system │ │ ├── text.hpp/cpp # Text rendering system
│ │ ├── gif.hpp/cpp # GIF image loader │ │ ├── gif.hpp/cpp # GIF image loader
│ │ ├── opengl/ # OpenGL shader backend │ │ ├── pixel_reveal.hpp # Pixel reveal effect
│ │ │ └── opengl_shader.hpp/cpp # CRT shader effects │ │ ├── render_info.hpp # Render information data
│ │ ├── palette_manager.hpp # Palette management
│ │ ├── sdl3gpu/ # SDL3 GPU shader backend
│ │ │ ├── sdl3gpu_shader.hpp/cpp # CRT/post-processing shader effects
│ │ │ └── *_spv.h # Pre-compiled SPIR-V shader headers
│ │ └── shader_backend.hpp # Abstract shader interface │ │ └── shader_backend.hpp # Abstract shader interface
│ ├── resources/ # Asset & Resource management │ ├── resources/ # Asset & Resource management
│ │ ├── asset.hpp/cpp # Asset registry (file path mapping) │ │ ├── resource_list.hpp # Asset path registry (O(1) lookups)
│ │ ── resource.hpp/cpp # Resource singleton (loads/caches assets) │ │ ── resource_cache.hpp # Resource caching singleton
│ │ ├── resource_loader.hpp # Asset loading logic
│ │ ├── resource_pack.hpp # Resource pack handling
│ │ ├── resource_helper.hpp # Resource utility functions
│ │ └── resource_types.hpp # Resource type definitions
│ └── system/ # System management │ └── system/ # System management
│ ├── director.hpp/cpp # Main application controller │ ├── director.hpp/cpp # Main application controller
│ ├── debug.hpp/cpp # Debug info overlay │ ├── debug.hpp/cpp # Debug info overlay
@@ -268,8 +276,13 @@ source/
│ │ ├── enemy.hpp/cpp # Enemy entities │ │ ├── enemy.hpp/cpp # Enemy entities
│ │ └── item.hpp/cpp # Collectible items │ │ └── item.hpp/cpp # Collectible items
│ ├── gameplay/ # Core gameplay systems │ ├── gameplay/ # Core gameplay systems
│ │ ├── room.hpp/cpp # Room/level logic, tilemap, collision │ │ ├── room.hpp/cpp # Room/level logic, collision
│ │ ├── room_loader.hpp/cpp # Room loading from YAML files
│ │ ├── room_tracker.hpp/cpp # Tracks visited rooms │ │ ├── room_tracker.hpp/cpp # Tracks visited rooms
│ │ ├── collision_map.hpp/cpp # Collision map data
│ │ ├── tilemap_renderer.hpp/cpp # Tilemap rendering
│ │ ├── enemy_manager.hpp/cpp # Enemy lifecycle management
│ │ ├── item_manager.hpp/cpp # Item lifecycle management
│ │ ├── scoreboard.hpp/cpp # Score display & data │ │ ├── scoreboard.hpp/cpp # Score display & data
│ │ ├── item_tracker.hpp/cpp # Tracks collected items │ │ ├── item_tracker.hpp/cpp # Tracks collected items
│ │ ├── stats.hpp/cpp # Game statistics │ │ ├── stats.hpp/cpp # Game statistics
@@ -284,18 +297,20 @@ source/
│ │ ├── ending2.hpp/cpp # Ending sequence 2 │ │ ├── ending2.hpp/cpp # Ending sequence 2
│ │ └── credits.hpp/cpp # Credits screen │ │ └── credits.hpp/cpp # Credits screen
│ ├── ui/ # User interface │ ├── ui/ # User interface
│ │ ── notifier.hpp/cpp # Achievement/notification display │ │ ── notifier.hpp/cpp # Achievement/notification display
│ │ ├── console.hpp/cpp # In-game debug console
│ │ └── console_commands.hpp/cpp # Console command definitions
│ ├── options.hpp/cpp # Game configuration/options │ ├── options.hpp/cpp # Game configuration/options
│ ├── game_control.hpp # Game control logic
│ ├── scene_manager.hpp # Scene flow state machine │ ├── scene_manager.hpp # Scene flow state machine
── defaults.hpp # Game defaults constants ── defaults.hpp # Game defaults constants
│ └── gameplay.hpp # Gameplay constants
├── external/ # Third-party libraries ├── external/ # Third-party libraries
│ ├── jail_audio.hpp/cpp # Custom audio library │ ├── fkyaml_node.hpp # YAML parsing library
│ ├── jail_audio.h # C interface for jail_audio
│ ├── stb_image.h # Image loading library │ ├── stb_image.h # Image loading library
│ └── stb_vorbis.h # OGG Vorbis audio decoding │ └── stb_vorbis.h # OGG Vorbis audio decoding
├── utils/ # Utility code ├── utils/ # Utility code
│ ├── delta_timer.hpp/cpp # Frame-rate independent timing │ ├── delta_timer.hpp/cpp # Frame-rate independent timing
│ ├── easing_functions.hpp # Easing/interpolation functions
│ ├── defines.hpp # Game constants (resolutions, block sizes) │ ├── defines.hpp # Game constants (resolutions, block sizes)
│ └── utils.hpp/cpp # Helper functions (colors, math) │ └── utils.hpp/cpp # Helper functions (colors, math)
└── main.cpp # Application entry point └── main.cpp # Application entry point
@@ -360,7 +375,7 @@ The game uses a scene manager to control application flow:
// namespace SceneManager // namespace SceneManager
enum class Scene { enum class Scene {
LOGO, LOADING_SCREEN, TITLE, CREDITS, GAME, DEMO, LOGO, LOADING_SCREEN, TITLE, CREDITS, GAME, DEMO,
GAME_OVER, ENDING, ENDING2, QUIT GAME_OVER, ENDING, ENDING2, RESTART_CURRENT, QUIT
}; };
inline Scene current = Scene::LOGO; // Global scene state inline Scene current = Scene::LOGO; // Global scene state
@@ -397,9 +412,10 @@ Display
**Key Components:** **Key Components:**
- `Surface` - 8-bit indexed pixel buffer with palette support - `Surface` - 8-bit indexed pixel buffer with palette support
- `SurfaceSprite` - Renders a fixed region of a surface - `Sprite` - Renders a fixed region of a surface
- `SurfaceAnimatedSprite` - Frame-based animation on top of sprite - `AnimatedSprite` - Frame-based animation on top of sprite
- `SurfaceMovingSprite` - Adds velocity/position to animated sprite - `MovingSprite` - Adds velocity/position to animated sprite
- `DissolveSprite` - Dissolve transition effect sprite
- Supports color replacement, palette swapping, and shader effects (CRT) - Supports color replacement, palette swapping, and shader effects (CRT)
### 3.5 Tile-Based Collision System ### 3.5 Tile-Based Collision System
@@ -439,8 +455,8 @@ struct AnimationData {
int counter; int counter;
}; };
// Loaded from .ani files (list of animation names) // Loaded from .yaml animation definition files
// Rendered with SurfaceAnimatedSprite // Rendered with AnimatedSprite
``` ```
--- ---
@@ -454,7 +470,7 @@ main()
Director::Director() [Initialization] Director::Director() [Initialization]
├─ Resource::List::init() - Initialize asset registry singleton ├─ Resource::List::init() - Initialize asset registry singleton
├─ Director::setFileList() - Load assets.yaml configuration (no verification) ├─ Director::setFileList() - Load assets.yaml via Resource::List (no verification)
├─ Options::loadFromFile() - Load game configuration ├─ Options::loadFromFile() - Load game configuration
├─ Audio::init() - Initialize SDL audio ├─ Audio::init() - Initialize SDL audio
├─ Screen::init() - Create window, SDL renderer ├─ Screen::init() - Create window, SDL renderer
@@ -510,7 +526,7 @@ Game::run() {
``` ```
Director::setFileList() Director::setFileList()
└─ Asset::loadFromFile(config_path, PREFIX, system_folder_) └─ Resource::List::loadFromFile(config_path, PREFIX, system_folder_)
├─ Read config/assets.yaml - Parse text configuration file ├─ Read config/assets.yaml - Parse text configuration file
├─ Parse YAML structure: assets grouped by category ├─ Parse YAML structure: assets grouped by category
├─ Replace variables (${PREFIX}, ${SYSTEM_FOLDER}) ├─ Replace variables (${PREFIX}, ${SYSTEM_FOLDER})
@@ -570,7 +586,7 @@ Game code
**Classes:** **Classes:**
- `PascalCase` for classes: `Player`, `Room`, `Screen`, `Director` - `PascalCase` for classes: `Player`, `Room`, `Screen`, `Director`
- Suffix `Sprite` for sprite classes: `SurfaceSprite`, `SurfaceAnimatedSprite` - Suffix `Sprite` for sprite classes: `Sprite`, `AnimatedSprite`, `MovingSprite`
**Methods:** **Methods:**
- `get*()` for getters: `getWidth()`, `getRect()` - `get*()` for getters: `getWidth()`, `getRect()`
@@ -613,8 +629,9 @@ Provides **time scaling** for slow-motion effects.
### 5.4 Palette System ### 5.4 Palette System
- 8-bit indexed color (256 colors per palette) - 8-bit indexed color (256 colors per palette)
- Multiple palettes can be loaded and swapped - Multiple palettes can be loaded and swapped via `PaletteManager`
- `Surface::setPalette()` changes rendering colors - `Screen::setPaletteByName()` changes the active palette
- Supports palette sort modes (by luminosity, similarity to Spectrum palette, etc.)
- Supports color replacement per-render: `renderWithColorReplace()` - Supports color replacement per-render: `renderWithColorReplace()`
- CRT shader effects can modify colors in real-time - CRT shader effects can modify colors in real-time
@@ -664,7 +681,7 @@ Achievements trigger notifications on unlock.
| Component | Technology | Version | Role | | Component | Technology | Version | Role |
|-----------|-----------|---------|------| |-----------|-----------|---------|------|
| **Graphics** | SDL3 | Latest | Window, rendering, input | | **Graphics** | SDL3 | Latest | Window, rendering, input |
| **GPU Rendering** | OpenGL | 3.2+ | Shader effects (CRT) | | **GPU Rendering** | SDL3 GPU | Latest | Shader effects (CRT, post-processing via SPIR-V) |
| **Audio** | SDL3 Audio | Latest | Audio device, mixing | | **Audio** | SDL3 Audio | Latest | Audio device, mixing |
| **Audio Decoding** | jail_audio (custom) | 1.x | OGG/WAV playback | | **Audio Decoding** | jail_audio (custom) | 1.x | OGG/WAV playback |
| **Image Loading** | stb_image | v2.x | PNG/GIF image loading | | **Image Loading** | stb_image | v2.x | PNG/GIF image loading |
@@ -700,6 +717,7 @@ Achievements trigger notifications on unlock.
| `Audio` | Music and SFX playback | Singleton | | `Audio` | Music and SFX playback | Singleton |
| `Resource::Cache` | Asset caching and loading (with detailed error messages) | Singleton | | `Resource::Cache` | Asset caching and loading (with detailed error messages) | Singleton |
| `Resource::List` | Asset path registry from config/assets.yaml, O(1) lookups, variable substitution | Singleton | | `Resource::List` | Asset path registry from config/assets.yaml, O(1) lookups, variable substitution | Singleton |
| `PaletteManager` | Palette loading, switching, and sorting | Managed by Screen |
| `Debug` | Debug overlay information | Singleton | | `Debug` | Debug overlay information | Singleton |
| `globalEvents` | Global SDL event handling (quit, device reset, mouse) | Namespace | | `globalEvents` | Global SDL event handling (quit, device reset, mouse) | Namespace |
@@ -709,7 +727,12 @@ Achievements trigger notifications on unlock.
|-------|---------| |-------|---------|
| `Game` | Main gameplay scene, orchestrates update/render | | `Game` | Main gameplay scene, orchestrates update/render |
| `Player` | Player entity with physics and animation | | `Player` | Player entity with physics and animation |
| `Room` | Level data, collision detection, tilemap rendering | | `Room` | Level data, collision detection |
| `RoomLoader` | Room loading from YAML files |
| `TilemapRenderer` | Tilemap rendering |
| `CollisionMap` | Collision map data |
| `EnemyManager` | Enemy lifecycle management |
| `ItemManager` | Item lifecycle management |
| `Enemy` | Enemy entity behavior and rendering | | `Enemy` | Enemy entity behavior and rendering |
| `Item` | Collectible items | | `Item` | Collectible items |
| `Scoreboard` | HUD display data | | `Scoreboard` | HUD display data |
@@ -722,17 +745,20 @@ Achievements trigger notifications on unlock.
| Class | Purpose | | Class | Purpose |
|-------|---------| |-------|---------|
| `Surface` | 8-bit indexed color pixel buffer with palette | | `Surface` | 8-bit indexed color pixel buffer with palette |
| `SurfaceSprite` | Renders a sprite region | | `Sprite` | Renders a sprite region |
| `SurfaceAnimatedSprite` | Frame-based animation rendering | | `AnimatedSprite` | Frame-based animation rendering |
| `SurfaceMovingSprite` | Sprite with velocity/position | | `MovingSprite` | Sprite with velocity/position |
| `DissolveSprite` | Dissolve transition sprite |
| `Text` | Text rendering system | | `Text` | Text rendering system |
| `OpenGLShader` | Shader compilation and effects | | `Sdl3gpuShader` | SDL3 GPU shader compilation and effects |
| `PaletteManager` | Palette loading and management |
### Utility Classes ### Utility Classes
| Class | Purpose | | Class/Header | Purpose |
|-------|---------| |-------|---------|
| `DeltaTimer` | Frame-rate independent timing | | `DeltaTimer` | Frame-rate independent timing |
| `easing_functions.hpp` | Easing/interpolation functions for animations |
--- ---
@@ -848,7 +874,7 @@ assets:
- type: BITMAP - type: BITMAP
path: ${PREFIX}/data/font/smb2.gif path: ${PREFIX}/data/font/smb2.gif
- type: FONT - type: FONT
path: ${PREFIX}/data/font/smb2.txt path: ${PREFIX}/data/font/smb2.fnt
# PLAYER # PLAYER
player: player:
@@ -892,23 +918,26 @@ assets:
- `${PREFIX}` - Replaced with `/../Resources` on macOS bundles, empty otherwise - `${PREFIX}` - Replaced with `/../Resources` on macOS bundles, empty otherwise
- `${SYSTEM_FOLDER}` - Replaced with user's system config folder - `${SYSTEM_FOLDER}` - Replaced with user's system config folder
### Animation Files (.ani) ### Animation Files (.yaml)
List of animation names, one per line: YAML-based animation definitions with frame data:
``` ```yaml
default animations:
jump - name: default
run frameWidth: 16
fall frameHeight: 16
speed: 100
loop: 0
frames: [...]
``` ```
### Room Data Files (.room) ### Room Data Files (.yaml)
Key-value pairs defining room properties: YAML-based room definitions:
``` ```yaml
number=01 room:
name=Starting Room name_en: Starting Room
bg_color=0x000000 bgColor: 0x000000
border_color=0xFF00FF connections: [...]
... tilemap: [...]
``` ```
### Tilemap Files (.tmx) ### Tilemap Files (.tmx)
@@ -923,7 +952,8 @@ Binary 256-color palette format (256 × 4 bytes RGBA).
### For Graphics Issues ### For Graphics Issues
- `Screen::render()` - Main rendering method - `Screen::render()` - Main rendering method
- `Screen::setPalete()` - Palette application - `Screen::setPaletteByName()` - Palette switching
- `PaletteManager` - Palette loading and sorting
- `Surface` class - Pixel buffer operations - `Surface` class - Pixel buffer operations
### For Input Issues ### For Input Issues
@@ -942,10 +972,10 @@ Binary 256-color palette format (256 × 4 bytes RGBA).
### For Asset Management ### For Asset Management
- `config/assets.yaml` - Asset configuration file (text-based, no recompilation needed) - `config/assets.yaml` - Asset configuration file (text-based, no recompilation needed)
- `Asset::loadFromFile()` - Loads assets from config file - `Resource::List::loadFromFile()` - Loads asset registry from config file
- `Resource::List::get()` - Retrieves asset path (O(1) lookup with unordered_map) - `Resource::List::get()` - Retrieves asset path (O(1) lookup with unordered_map)
- `Resource::load()` - Asset loading - `Resource::Cache` - Asset loading and caching
- `Director::setFileList()` - Calls `Asset::loadFromFile()` with PREFIX and system_folder - `Director::setFileList()` - Calls `Resource::List::loadFromFile()` with PREFIX and system_folder
--- ---
@@ -985,6 +1015,6 @@ Binary 256-color palette format (256 × 4 bytes RGBA).
--- ---
**Last Updated:** November 2022 (per README) **Last Updated:** April 2026
**Original Author:** JailDesigner **Original Author:** JailDesigner
**Repository:** Gitea (internal) **Repository:** Gitea (internal)

View File

@@ -96,8 +96,14 @@ set(APP_SOURCES
source/game/scenes/logo.cpp source/game/scenes/logo.cpp
source/game/scenes/title.cpp source/game/scenes/title.cpp
# Game - Editor (debug only, guarded by #ifdef _DEBUG in source)
source/game/editor/map_editor.cpp
source/game/editor/editor_statusbar.cpp
source/game/editor/room_saver.cpp
# Game - UI # Game - UI
source/game/ui/console.cpp source/game/ui/console.cpp
source/game/ui/console_commands.cpp
source/game/ui/notifier.cpp source/game/ui/notifier.cpp
# Utils # Utils
@@ -224,7 +230,17 @@ if(WIN32)
elseif(APPLE) elseif(APPLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUILD) target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUILD)
target_compile_options(${PROJECT_NAME} PRIVATE -Wno-deprecated) target_compile_options(${PROJECT_NAME} PRIVATE -Wno-deprecated)
set(CMAKE_OSX_ARCHITECTURES "arm64") if(NOT CMAKE_OSX_ARCHITECTURES)
set(CMAKE_OSX_ARCHITECTURES "arm64")
endif()
if(MACOS_BUNDLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUNDLE)
target_link_options(${PROJECT_NAME} PRIVATE
-framework SDL3
-F ${CMAKE_SOURCE_DIR}/release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64
-rpath @executable_path/../Frameworks/
)
endif()
elseif(UNIX AND NOT APPLE) elseif(UNIX AND NOT APPLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD) target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
endif() endif()

286
Makefile
View File

@@ -18,24 +18,13 @@ RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
RESOURCE_FILE := release/windows/jdd.res RESOURCE_FILE := release/windows/jdd.res
# ============================================================================== # ==============================================================================
# PACKING TOOL # TOOLS
# ==============================================================================
ifeq ($(OS),Windows_NT)
PACK_TOOL := $(DIR_TOOLS)pack_resources/pack_resources.exe
PACK_CXX := $(CXX)
else
PACK_TOOL := $(DIR_TOOLS)pack_resources/pack_resources
PACK_CXX := $(CXX)
endif
PACK_SOURCES := $(DIR_TOOLS)pack_resources/pack_resources.cpp source/core/resources/resource_pack.cpp
PACK_INCLUDES := -Isource
# ==============================================================================
# SHADERS
# ============================================================================== # ==============================================================================
DIR_PACK_TOOL := $(DIR_TOOLS)pack_resources
SHADER_SCRIPT := $(DIR_ROOT)tools/shaders/compile_spirv.sh SHADER_SCRIPT := $(DIR_ROOT)tools/shaders/compile_spirv.sh
SHADER_VERT_H := $(DIR_ROOT)source/core/rendering/sdl3gpu/postfx_vert_spv.h SHADER_CMAKE := $(DIR_ROOT)tools/shaders/compile_spirv.cmake
SHADER_FRAG_H := $(DIR_ROOT)source/core/rendering/sdl3gpu/postfx_frag_spv.h SHADERS_DIR := $(DIR_ROOT)data/shaders
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
GLSLC := $(shell where glslc 2>NUL) GLSLC := $(shell where glslc 2>NUL)
else else
@@ -79,190 +68,87 @@ MACOS_APPLE_SILICON_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-macos-apple
LINUX_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux.tar.gz LINUX_RELEASE := $(DIST_DIR)/$(TARGET_NAME)-$(VERSION)-linux.tar.gz
# ============================================================================== # ==============================================================================
# SOURCE FILES # PLATAFORMA
# ============================================================================== # ==============================================================================
APP_SOURCES := \
source/main.cpp \
source/core/audio/audio.cpp \
source/core/input/global_inputs.cpp \
source/core/input/input_types.cpp \
source/core/input/input.cpp \
source/core/input/mouse.cpp \
source/core/locale/locale.cpp \
source/core/rendering/gif.cpp \
source/core/rendering/palette_manager.cpp \
source/core/rendering/pixel_reveal.cpp \
source/core/rendering/render_info.cpp \
source/core/rendering/screen.cpp \
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp \
source/core/rendering/sprite/animated_sprite.cpp \
source/core/rendering/sprite/dissolve_sprite.cpp \
source/core/rendering/sprite/moving_sprite.cpp \
source/core/rendering/sprite/sprite.cpp \
source/core/rendering/surface.cpp \
source/core/rendering/text.cpp \
source/core/resources/resource_cache.cpp \
source/core/resources/resource_helper.cpp \
source/core/resources/resource_list.cpp \
source/core/resources/resource_loader.cpp \
source/core/resources/resource_pack.cpp \
source/core/system/debug.cpp \
source/core/system/director.cpp \
source/core/system/global_events.cpp \
source/game/entities/enemy.cpp \
source/game/entities/item.cpp \
source/game/entities/player.cpp \
source/game/gameplay/cheevos.cpp \
source/game/gameplay/collision_map.cpp \
source/game/gameplay/enemy_manager.cpp \
source/game/gameplay/item_manager.cpp \
source/game/gameplay/item_tracker.cpp \
source/game/gameplay/room_loader.cpp \
source/game/gameplay/room_tracker.cpp \
source/game/gameplay/room.cpp \
source/game/gameplay/scoreboard.cpp \
source/game/gameplay/stats.cpp \
source/game/gameplay/tilemap_renderer.cpp \
source/game/options.cpp \
source/game/scenes/credits.cpp \
source/game/scenes/ending.cpp \
source/game/scenes/ending2.cpp \
source/game/scenes/game_over.cpp \
source/game/scenes/game.cpp \
source/game/scenes/loading_screen.cpp \
source/game/scenes/logo.cpp \
source/game/scenes/title.cpp \
source/game/ui/console.cpp \
source/game/ui/notifier.cpp \
source/utils/delta_timer.cpp \
source/utils/utils.cpp
# All sources combined
ALL_SOURCES := $(APP_SOURCES)
# ==============================================================================
# INCLUDES
# ==============================================================================
INCLUDES := -Isource
# ==============================================================================
# COMPILER FLAGS (OS-specific)
# ==============================================================================
CPP_STANDARD := c++20
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
FixPath = $(subst /,\\,$1) FixPath = $(subst /,\\,$1)
CXXFLAGS := -std=$(CPP_STANDARD) -Wall -Os -ffunction-sections -fdata-sections \
-Wl,--gc-sections -static-libstdc++ -static-libgcc \
-Wl,-subsystem,windows -DWINDOWS_BUILD
CXXFLAGS_DEBUG := -std=$(CPP_STANDARD) -Wall -g -D_DEBUG -DWINDOWS_BUILD
LDFLAGS := -lmingw32 -lws2_32 -lSDL3
RM := del /Q RM := del /Q
MKDIR := mkdir MKDIR := mkdir
else else
FixPath = $1 FixPath = $1
CXXFLAGS := -std=$(CPP_STANDARD) -Wall -Os -ffunction-sections -fdata-sections
CXXFLAGS_DEBUG := -std=$(CPP_STANDARD) -Wall -g -D_DEBUG
LDFLAGS := -lSDL3
RMFILE := rm -f RMFILE := rm -f
RMDIR := rm -rdf RMDIR := rm -rdf
MKDIR := mkdir -p MKDIR := mkdir -p
UNAME_S := $(shell uname -s) UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
CXXFLAGS += -DLINUX_BUILD
endif
ifeq ($(UNAME_S),Darwin)
CXXFLAGS += -DMACOS_BUILD
CXXFLAGS_DEBUG += -DMACOS_BUILD
# Configurar arquitectura (por defecto arm64)
CXXFLAGS += -arch arm64
CXXFLAGS_DEBUG += -arch arm64
# Usar Apple Clang explícitamente para evitar LLVM de Homebrew
MACOS_CXX := /usr/bin/clang++
endif
endif endif
# ============================================================================== # ==============================================================================
# REGLAS PARA COMPILACIÓN DE SHADERS # COMPILACIÓN CON CMAKE
# ==============================================================================
all:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake --build build
debug:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
@cmake --build build
# ==============================================================================
# RELEASE AUTOMÁTICO (detecta SO)
# ==============================================================================
release:
ifeq ($(OS),Windows_NT)
@"$(MAKE)" windows_release
else
ifeq ($(UNAME_S),Darwin)
@$(MAKE) macos_release
else
@$(MAKE) linux_release
endif
endif
# ==============================================================================
# REGLAS PARA COMPILACIÓN DE SHADERS (multiplataforma via cmake)
# ============================================================================== # ==============================================================================
compile_shaders: compile_shaders:
ifdef GLSLC ifdef GLSLC
ifeq ($(OS),Windows_NT) @cmake -D GLSLC=$(GLSLC) -D SHADERS_DIR=$(SHADERS_DIR) -D HEADERS_DIR=$(HEADERS_DIR) -P $(SHADER_CMAKE)
@powershell -Command "if ((Test-Path '$(SHADER_VERT_H)') -and (Test-Path '$(SHADER_FRAG_H)')) { Write-Host 'Shaders SPIR-V precompilados OK' } else { Write-Host 'Compilando shaders SPIR-V...'; bash '$(SHADER_SCRIPT)'; Write-Host 'Shaders compilados' }"
else else
@echo "Compilando shaders SPIR-V..." @echo "glslc no encontrado - asegurate de que los headers SPIR-V precompilados existen"
@$(SHADER_SCRIPT)
@echo "✓ Shaders compilados"
endif
else
ifeq ($(OS),Windows_NT)
@powershell -Command "if ((Test-Path '$(SHADER_VERT_H)') -and (Test-Path '$(SHADER_FRAG_H)')) { Write-Host 'glslc no encontrado - usando headers SPIR-V precompilados' } else { Write-Host 'ERROR: glslc no encontrado y headers SPIR-V no existen.'; Write-Host ' Instala glslc o ejecuta: tools/shaders/compile_spirv.sh'; exit 1 }"
else
@if [ -f "$(SHADER_VERT_H)" ] && [ -f "$(SHADER_FRAG_H)" ]; then \
echo "⚠ glslc no encontrado - usando headers SPIR-V precompilados"; \
else \
echo "ERROR: glslc no encontrado y headers SPIR-V no existen."; \
echo " Instala glslc: sudo apt install glslang-tools"; \
echo " O ejecuta manualmente: tools/shaders/compile_spirv.sh"; \
exit 1; \
fi
endif
endif endif
# ============================================================================== # ==============================================================================
# REGLAS PARA HERRAMIENTA DE EMPAQUETADO Y RESOURCES.PACK # REGLAS PARA HERRAMIENTA DE EMPAQUETADO Y RESOURCES.PACK
# ============================================================================== # ==============================================================================
$(PACK_TOOL): FORCE pack_tool:
@echo "Compilando herramienta de empaquetado..." @$(MAKE) -C $(DIR_PACK_TOOL)
$(PACK_CXX) -std=$(CPP_STANDARD) -Wall -Os $(PACK_INCLUDES) $(PACK_SOURCES) -o $(PACK_TOOL)
@echo "✓ Herramienta de empaquetado lista: $(PACK_TOOL)"
pack_tool: $(PACK_TOOL) resources.pack: pack_tool
@$(MAKE) -C $(DIR_PACK_TOOL) pack
resources.pack: $(PACK_TOOL)
@echo "Generando resources.pack desde directorio data/..."
$(PACK_TOOL) data resources.pack
@echo "✓ resources.pack generado exitosamente"
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA WINDOWS # COMPILACIÓN PARA WINDOWS (RELEASE)
# ============================================================================== # ==============================================================================
windows:
@echo off
@echo Generando version.h...
@powershell -Command "$$GIT_HASH = (git rev-parse --short=7 HEAD 2>$$null); if (-not $$GIT_HASH) { $$GIT_HASH = 'unknown' }; (Get-Content source/version.h.in) -replace '@GIT_HASH@', $$GIT_HASH | Set-Content source/version.h"
@echo Compilando para Windows con nombre: "$(WIN_TARGET_FILE).exe"
windres release/windows/jdd.rc -O coff -o $(RESOURCE_FILE)
g++ $(ALL_SOURCES) $(RESOURCE_FILE) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(WIN_TARGET_FILE).exe"
strip -s -R .comment -R .gnu.version "$(WIN_TARGET_FILE).exe" --strip-unneeded
windows_release: windows_release:
@"$(MAKE)" compile_shaders
@"$(MAKE)" resources.pack
@echo off @echo off
@echo Creando release para Windows - Version: $(VERSION) @echo Creando release para Windows - Version: $(VERSION)
# Generate version.h from version.h.in # Compila con cmake (genera shaders, resources.pack y ejecutable)
@echo "Generando version.h..." @cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@powershell -Command "$$GIT_HASH = (git rev-parse --short=7 HEAD 2>$$null); if (-not $$GIT_HASH) { $$GIT_HASH = 'unknown' }; (Get-Content source/version.h.in) -replace '@GIT_HASH@', $$GIT_HASH | Set-Content source/version.h" @cmake --build build
# Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER' # Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER'
@powershell -Command "if (-not (Test-Path '$(DIST_DIR)')) {New-Item '$(DIST_DIR)' -ItemType Directory}" @powershell -Command "if (-not (Test-Path '$(DIST_DIR)')) {New-Item '$(DIST_DIR)' -ItemType Directory}"
@powershell -Command "if (Test-Path '$(RELEASE_FOLDER)') {Remove-Item '$(RELEASE_FOLDER)' -Recurse -Force}" @powershell -Command "if (Test-Path '$(RELEASE_FOLDER)') {Remove-Item '$(RELEASE_FOLDER)' -Recurse -Force}"
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}" @powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
# Copia el archivo 'resources.pack' # Copia ficheros
@powershell -Command "Copy-Item -Path 'resources.pack' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item -Path 'resources.pack' -Destination '$(RELEASE_FOLDER)'"
# Copia los ficheros que están en la raíz del proyecto
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item -Path '$(TARGET_FILE)' -Destination '\"$(WIN_RELEASE_FILE).exe\"'"
# Compila (con icono)
windres release/windows/jdd.rc -O coff -o $(RESOURCE_FILE)
g++ $(ALL_SOURCES) $(RESOURCE_FILE) $(INCLUDES) -DRELEASE_BUILD $(CXXFLAGS) $(LDFLAGS) -o "$(WIN_RELEASE_FILE).exe"
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
# Crea el fichero .zip # Crea el fichero .zip
@@ -274,26 +160,17 @@ windows_release:
@powershell -Command "if (Test-Path '$(RELEASE_FOLDER)') {Remove-Item '$(RELEASE_FOLDER)' -Recurse -Force}" @powershell -Command "if (Test-Path '$(RELEASE_FOLDER)') {Remove-Item '$(RELEASE_FOLDER)' -Recurse -Force}"
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA MACOS # COMPILACIÓN PARA MACOS (RELEASE)
# ============================================================================== # ==============================================================================
macos:
@GIT_HASH=$$(git rev-parse --short=7 HEAD 2>/dev/null || echo "unknown"); \
sed "s/@GIT_HASH@/$$GIT_HASH/g" source/version.h.in > source/version.h
@echo "Compilando para macOS: $(TARGET_NAME)"
$(MACOS_CXX) $(ALL_SOURCES) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(TARGET_FILE)"
macos_release: macos_release:
@$(MAKE) compile_shaders
@$(MAKE) resources.pack
@echo "Creando release para macOS - Version: $(VERSION)" @echo "Creando release para macOS - Version: $(VERSION)"
# Verificar e instalar create-dmg si es necesario # Verificar e instalar create-dmg si es necesario
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg) @which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
# Generate version.h from version.h.in # Compila la versión para procesadores Intel con cmake (genera shaders y resources.pack)
@echo "Generando version.h..." @cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON
@GIT_HASH=$$(git rev-parse --short=7 HEAD 2>/dev/null || echo "unknown"); \ @cmake --build build/intel
sed "s/@GIT_HASH@/$$GIT_HASH/g" source/version.h.in > source/version.h
# Elimina datos de compilaciones anteriores # Elimina datos de compilaciones anteriores
$(RMDIR) "$(RELEASE_FOLDER)" $(RMDIR) "$(RELEASE_FOLDER)"
@@ -322,8 +199,8 @@ macos_release:
sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \ sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \
sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist" sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
# Compila la versión para procesadores Intel # Copia el ejecutable Intel al bundle
$(MACOS_CXX) $(ALL_SOURCES) $(INCLUDES) -DMACOS_BUNDLE -DRELEASE_BUILD -std=$(CPP_STANDARD) -Wall -Os -framework SDL3 -F release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64 -ffunction-sections -fdata-sections -o "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" -rpath @executable_path/../Frameworks/ -target x86_64-apple-macos10.15 cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
# Firma la aplicación # Firma la aplicación
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app" codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
@@ -345,8 +222,10 @@ macos_release:
"$(RELEASE_FOLDER)" || true "$(RELEASE_FOLDER)" || true
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)" @echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
# Compila la versión para procesadores Apple Silicon # Compila la versión para procesadores Apple Silicon con cmake
$(MACOS_CXX) $(ALL_SOURCES) $(INCLUDES) -DMACOS_BUNDLE -DRELEASE_BUILD -std=$(CPP_STANDARD) -Wall -Os -framework SDL3 -F release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64 -ffunction-sections -fdata-sections -o "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" -rpath @executable_path/../Frameworks/ -target arm64-apple-macos11 @cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON
@cmake --build build/arm
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
# Firma la aplicación # Firma la aplicación
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app" codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
@@ -370,27 +249,19 @@ macos_release:
# Elimina las carpetas temporales # Elimina las carpetas temporales
$(RMDIR) "$(RELEASE_FOLDER)" $(RMDIR) "$(RELEASE_FOLDER)"
$(RMDIR) build/intel
$(RMDIR) build/arm
$(RMFILE) "$(DIST_DIR)"/rw.* $(RMFILE) "$(DIST_DIR)"/rw.*
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA LINUX # COMPILACIÓN PARA LINUX (RELEASE)
# ============================================================================== # ==============================================================================
linux:
@GIT_HASH=$$(git rev-parse --short=7 HEAD 2>/dev/null || echo "unknown"); \
sed "s/@GIT_HASH@/$$GIT_HASH/g" source/version.h.in > source/version.h
@echo "Compilando para Linux: $(TARGET_NAME)"
g++ $(ALL_SOURCES) $(INCLUDES) $(CXXFLAGS) $(LDFLAGS) -o "$(TARGET_FILE)"
strip -s -R .comment -R .gnu.version "$(TARGET_FILE)" --strip-unneeded
linux_release: linux_release:
@$(MAKE) compile_shaders
@$(MAKE) resources.pack
@echo "Creando release para Linux - Version: $(VERSION)" @echo "Creando release para Linux - Version: $(VERSION)"
# Generate version.h from version.h.in # Compila con cmake (genera shaders, resources.pack y ejecutable)
@echo "Generando version.h..." @cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@GIT_HASH=$$(git rev-parse --short=7 HEAD 2>/dev/null || echo "unknown"); \ @cmake --build build
sed "s/@GIT_HASH@/$$GIT_HASH/g" source/version.h.in > source/version.h
# Elimina carpeta temporal previa y la recrea (crea dist/ si no existe) # Elimina carpeta temporal previa y la recrea (crea dist/ si no existe)
$(RMDIR) "$(RELEASE_FOLDER)" $(RMDIR) "$(RELEASE_FOLDER)"
@@ -401,9 +272,7 @@ linux_release:
cp LICENSE "$(RELEASE_FOLDER)" cp LICENSE "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)" cp README.md "$(RELEASE_FOLDER)"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)" cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
# Compila
g++ $(ALL_SOURCES) $(INCLUDES) -DRELEASE_BUILD $(CXXFLAGS) $(LDFLAGS) -o "$(RELEASE_FILE)"
strip -s -R .comment -R .gnu.version "$(RELEASE_FILE)" --strip-unneeded strip -s -R .comment -R .gnu.version "$(RELEASE_FILE)" --strip-unneeded
# Empaqueta ficheros # Empaqueta ficheros
@@ -425,17 +294,24 @@ show_version:
help: help:
@echo "Makefile para JailDoctor's Dilemma" @echo "Makefile para JailDoctor's Dilemma"
@echo "Comandos disponibles:" @echo "Comandos disponibles:"
@echo " windows - Compilar para Windows" @echo ""
@echo " windows_release - Crear release completo para Windows" @echo " Compilacion:"
@echo " linux - Compilar para Linux" @echo " make - Compilar con cmake (Release)"
@echo " linux_release - Crear release completo para Linux" @echo " make debug - Compilar con cmake (Debug)"
@echo " macos - Compilar para macOS" @echo ""
@echo " macos_release - Crear release completo para macOS" @echo " Release:"
@echo " pack_tool - Compilar herramienta de empaquetado" @echo " make release - Crear release (detecta SO automaticamente)"
@echo " resources.pack - Generar pack de recursos desde data/" @echo " make windows_release - Crear release para Windows"
@echo " show_version - Mostrar version actual ($(VERSION))" @echo " make linux_release - Crear release para Linux"
@echo " help - Mostrar esta ayuda" @echo " make macos_release - Crear release para macOS"
@echo ""
@echo " Herramientas:"
@echo " make compile_shaders - Compilar shaders SPIR-V"
@echo " make pack_tool - Compilar herramienta de empaquetado"
@echo " make resources.pack - Generar pack de recursos desde data/"
@echo ""
@echo " Otros:"
@echo " make show_version - Mostrar version actual ($(VERSION))"
@echo " make help - Mostrar esta ayuda"
FORCE: .PHONY: all debug release windows_release macos_release linux_release compile_shaders pack_tool resources.pack show_version help
.PHONY: windows windows_release macos macos_release linux linux_release compile_shaders pack_tool resources.pack show_version help

View File

@@ -32,7 +32,7 @@ assets:
- type: PALETTE - type: PALETTE
path: ${PREFIX}/data/palette/zx-spectrum-adjusted.pal path: ${PREFIX}/data/palette/zx-spectrum-adjusted.pal
- type: PALETTE - type: PALETTE
path: ${PREFIX}/data/palette/zxarne-5-2.pal path: ${PREFIX}/data/palette/zxarne-5.2.pal
- type: PALETTE - type: PALETTE
path: ${PREFIX}/data/palette/black-and-white.pal path: ${PREFIX}/data/palette/black-and-white.pal
- type: PALETTE - type: PALETTE
@@ -46,15 +46,33 @@ assets:
- type: PALETTE - type: PALETTE
path: ${PREFIX}/data/palette/pico-8.pal path: ${PREFIX}/data/palette/pico-8.pal
- type: PALETTE - type: PALETTE
path: ${PREFIX}/data/palette/sweetie-16.pal path: ${PREFIX}/data/palette/sweetie.pal
- type: PALETTE - type: PALETTE
path: ${PREFIX}/data/palette/island-joy-16.pal path: ${PREFIX}/data/palette/island-joy.pal
- type: PALETTE - type: PALETTE
path: ${PREFIX}/data/palette/lost-century.pal path: ${PREFIX}/data/palette/lost-century.pal
- type: PALETTE - type: PALETTE
path: ${PREFIX}/data/palette/na16.pal path: ${PREFIX}/data/palette/na.pal
- type: PALETTE - type: PALETTE
path: ${PREFIX}/data/palette/steam-lords.pal path: ${PREFIX}/data/palette/steam-lords.pal
- type: PALETTE
path: ${PREFIX}/data/palette/winds-seed-pc98.pal
- type: PALETTE
path: ${PREFIX}/data/palette/psychic-fibre.pal
- type: PALETTE
path: ${PREFIX}/data/palette/shido-cyberneon.pal
- type: PALETTE
path: ${PREFIX}/data/palette/darkseed.pal
- type: PALETTE
path: ${PREFIX}/data/palette/antiquity.pal
- type: PALETTE
path: ${PREFIX}/data/palette/bubblegum.pal
- type: PALETTE
path: ${PREFIX}/data/palette/vanilla-milkshake.pal
- type: PALETTE
path: ${PREFIX}/data/palette/aged-terracotta.pal
- type: PALETTE
path: ${PREFIX}/data/palette/h16da.pal
# LOCALE # LOCALE
locale: locale:
@@ -99,6 +117,11 @@ assets:
required: false required: false
absolute: true absolute: true
# CONSOLE
console:
- type: DATA
path: ${PREFIX}/data/console/commands.yaml
# ROOMS # ROOMS
rooms: rooms:
- type: ROOM - type: ROOM

256
data/console/commands.yaml Normal file
View File

@@ -0,0 +1,256 @@
# JailDoctor's Dilemma - Console Commands
# Metadata for the in-game console command system.
# Execution logic stays in C++; this file defines metadata only.
#
# Fields:
# keyword - Command name (uppercase)
# handler - C++ handler function identifier
# description - Short description for help output
# usage - Full usage string for terminal help
# instant - (optional) Skip typewriter effect (default: false)
# hidden - (optional) Hide from TAB completion (default: false)
# debug_only - (optional) Only available in debug builds (default: false)
# help_hidden - (optional) Don't show in help output (default: false)
# dynamic_completions - (optional) Completions generated at runtime (default: false)
# completions - (optional) Static TAB completion tree
# debug_extras - (optional) Overrides applied in debug builds
categories:
- name: VIDEO
commands:
- keyword: SS
handler: cmd_ss
description: Supersampling
usage: "SS [ON|OFF|SIZE|UPSCALE [NEAREST|LINEAR]|DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]]"
completions:
SS: [ON, OFF, SIZE, UPSCALE, DOWNSCALE]
SS UPSCALE: [NEAREST, LINEAR]
SS DOWNSCALE: [BILINEAR, LANCZOS2, LANCZOS3]
- keyword: SHADER
handler: cmd_shader
description: "Toggle/select shader (F4)"
usage: "SHADER [ON|OFF|NEXT|POSTFX|CRTPI|PRESET [NEXT|PREV|<name>]]"
completions:
SHADER: [ON, OFF, NEXT, POSTFX, CRTPI, PRESET]
dynamic_completions: true
- keyword: BORDER
handler: cmd_border
description: "Decorative border (B)"
usage: "BORDER [ON|OFF]"
completions:
BORDER: [ON, OFF]
- keyword: FULLSCREEN
handler: cmd_fullscreen
description: "Fullscreen mode (F3)"
usage: "FULLSCREEN [ON|OFF]"
completions:
FULLSCREEN: [ON, OFF]
- keyword: ZOOM
handler: cmd_zoom
description: "Window zoom (F1/F2)"
usage: "ZOOM [UP|DOWN|<1-N>]"
completions:
ZOOM: [UP, DOWN]
- keyword: INTSCALE
handler: cmd_intscale
description: "Integer scaling (F7)"
usage: "INTSCALE [ON|OFF]"
completions:
INTSCALE: [ON, OFF]
- keyword: VSYNC
handler: cmd_vsync
description: "Vertical sync"
usage: "VSYNC [ON|OFF]"
completions:
VSYNC: [ON, OFF]
- keyword: DRIVER
handler: cmd_driver
description: "GPU driver (restart to apply)"
usage: "DRIVER [LIST|AUTO|NONE|<name>]"
completions:
DRIVER: [LIST, AUTO, NONE]
- keyword: PALETTE
handler: cmd_palette
description: "Color palette (F5/F6)"
usage: "PALETTE [NEXT|PREV|SORT [ORIGINAL|LUMINANCE|SPECTRUM]|DEFAULT|<name>]"
completions:
PALETTE SORT: [ORIGINAL, LUMINANCE, SPECTRUM]
dynamic_completions: true
- name: AUDIO
commands:
- keyword: AUDIO
handler: cmd_audio
description: Audio master
usage: "AUDIO [ON|OFF|VOL <0-100>]"
completions:
AUDIO: [ON, OFF, VOL]
- keyword: MUSIC
handler: cmd_music
description: Music volume
usage: "MUSIC [ON|OFF|VOL <0-100>]"
completions:
MUSIC: [ON, OFF, VOL]
- keyword: SOUND
handler: cmd_sound
description: Sound volume
usage: "SOUND [ON|OFF|VOL <0-100>]"
completions:
SOUND: [ON, OFF, VOL]
- name: GAME
commands:
- keyword: PLAYER
handler: cmd_player
description: "Player skin and color"
usage: "PLAYER SKIN <name> | PLAYER COLOR <0-15>|DEFAULT"
completions:
PLAYER: [SKIN, COLOR]
PLAYER SKIN: [DEFAULT, ABAD, BATMAN, CHIP, CONGO, JEANNINE, MUMMY, UPV_STUDENT]
PLAYER COLOR: [DEFAULT]
- keyword: RESTART
handler: cmd_restart
description: Restart from the beginning
usage: RESTART
instant: true
- keyword: KIOSK
handler: cmd_kiosk
description: Enable kiosk mode
usage: "KIOSK [ON]"
completions:
KIOSK: [ON]
- keyword: EXIT
handler: cmd_exit
description: Quit application
usage: EXIT
instant: true
- keyword: QUIT
handler: cmd_quit
description: Quit application
usage: QUIT
instant: true
help_hidden: true
- name: INFO
commands:
- keyword: SHOW
handler: cmd_show
description: Show info overlay
usage: "SHOW [INFO]"
completions:
SHOW: [INFO]
debug_extras:
description: "Show overlay/test notification"
usage: "SHOW [INFO|NOTIFICATION|CHEEVO]"
completions:
SHOW: [INFO, NOTIFICATION, CHEEVO]
- keyword: HIDE
handler: cmd_hide
description: Hide info overlay
usage: "HIDE [INFO]"
completions:
HIDE: [INFO]
- keyword: SIZE
handler: cmd_size
description: Window size in pixels
usage: SIZE
- keyword: HELP
handler: cmd_help
description: "Show this help"
usage: "HELP / ?"
- keyword: "?"
handler: cmd_help
help_hidden: true
- name: DEBUG
debug_only: true
commands:
- keyword: DEBUG
handler: cmd_debug
description: "Debug mode and start options (F12)"
usage: "DEBUG [MODE [ON|OFF]|START [HERE|ROOM|POS|SCENE <name>]]"
completions:
DEBUG: [MODE, START]
DEBUG MODE: [ON, OFF]
DEBUG START: [HERE, ROOM, POS, SCENE]
DEBUG START SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, ENDING, ENDING2]
- keyword: ITEMS
handler: cmd_items
description: "Set item count (GAME only)"
usage: "ITEMS <0-200>"
- keyword: ROOM
handler: cmd_room
description: "Change to room number (GAME only)"
usage: "ROOM <1-60>|NEXT|PREV|LEFT|RIGHT|UP|DOWN"
completions:
ROOM: [NEXT, PREV, LEFT, RIGHT, UP, DOWN]
- keyword: SCENE
handler: cmd_scene
description: Change scene
usage: "SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART]"
completions:
SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, ENDING, ENDING2, RESTART]
- keyword: EDIT
handler: cmd_edit
description: "Map editor mode (GAME only)"
usage: "EDIT [ON|OFF|REVERT]"
completions:
EDIT: [ON, OFF, REVERT]
- keyword: ENEMY
handler: cmd_enemy
description: "Add, delete or duplicate enemy (editor)"
usage: "ENEMY <ADD|DELETE|DUPLICATE>"
completions:
ENEMY: [ADD, DELETE, DUPLICATE]
- keyword: SET
handler: cmd_set
description: "Set enemy property (editor, select enemy first)"
usage: "SET <ANIMATION|COLOR|VX|VY|FLIP|MIRROR> <value>"
dynamic_completions: true
completions:
SET: [ANIMATION, COLOR, VX, VY, FLIP, MIRROR]
SET FLIP: [ON, OFF]
SET MIRROR: [ON, OFF]
- name: CHEATS
commands:
- keyword: CHEAT
handler: cmd_cheat
description: "Game cheats (GAME only)"
usage: "CHEAT [INFINITE LIVES|INVINCIBILITY|OPEN THE JAIL|CLOSE THE JAIL]"
hidden: true
completions:
CHEAT: [INFINITE, INVINCIBILITY, OPEN, CLOSE]
CHEAT INFINITE: [LIVES]
CHEAT INFINITE LIVES: [ON, OFF]
CHEAT INVINCIBILITY: [ON, OFF]
CHEAT OPEN: [THE]
CHEAT OPEN THE: [JAIL]
CHEAT CLOSE: [THE]
CHEAT CLOSE THE: [JAIL]
debug_extras:
hidden: false

View File

@@ -116,6 +116,7 @@ ui:
supersampling_enabled: "SUPERMOSTREIG ACTIVAT" supersampling_enabled: "SUPERMOSTREIG ACTIVAT"
supersampling_disabled: "SUPERMOSTREIG DESACTIVAT" supersampling_disabled: "SUPERMOSTREIG DESACTIVAT"
palette: "PALETA" palette: "PALETA"
palette_sort: "ORDENACIÓ PALETA"
integer_scale_enabled: "ESCALAT SENCER ACTIVAT" integer_scale_enabled: "ESCALAT SENCER ACTIVAT"
integer_scale_disabled: "ESCALAT SENCER DESACTIVAT" integer_scale_disabled: "ESCALAT SENCER DESACTIVAT"
vsync_enabled: "V-SYNC ACTIVAT" vsync_enabled: "V-SYNC ACTIVAT"
@@ -125,6 +126,8 @@ scoreboard:
items: "TRESORS PILLATS " items: "TRESORS PILLATS "
time: " HORA " time: " HORA "
rooms: "SALES" rooms: "SALES"
cheat_infinite_lives: "vides inf"
cheat_invincibility: "inv"
game: game:
music_enabled: "MÚSICA ACTIVADA" music_enabled: "MÚSICA ACTIVADA"
@@ -138,3 +141,5 @@ game:
cheat_jail_open: "JAIL OBERTA" cheat_jail_open: "JAIL OBERTA"
debug_enabled: "DEBUG ACTIVAT" debug_enabled: "DEBUG ACTIVAT"
debug_disabled: "DEBUG DESACTIVAT" debug_disabled: "DEBUG DESACTIVAT"
editor_enabled: "EDITOR ACTIVAT"
editor_disabled: "EDITOR DESACTIVAT"

View File

@@ -116,6 +116,7 @@ ui:
supersampling_enabled: "SUPERSAMPLING ON" supersampling_enabled: "SUPERSAMPLING ON"
supersampling_disabled: "SUPERSAMPLING OFF" supersampling_disabled: "SUPERSAMPLING OFF"
palette: "PALETTE" palette: "PALETTE"
palette_sort: "PALETTE SORT"
integer_scale_enabled: "INTEGER SCALE ENABLED" integer_scale_enabled: "INTEGER SCALE ENABLED"
integer_scale_disabled: "INTEGER SCALE DISABLED" integer_scale_disabled: "INTEGER SCALE DISABLED"
vsync_enabled: "V-SYNC ENABLED" vsync_enabled: "V-SYNC ENABLED"
@@ -125,6 +126,8 @@ scoreboard:
items: "ITEMS COLLECTED " items: "ITEMS COLLECTED "
time: " TIME " time: " TIME "
rooms: "ROOMS" rooms: "ROOMS"
cheat_infinite_lives: "inf lives"
cheat_invincibility: "inv"
game: game:
music_enabled: "MUSIC ENABLED" music_enabled: "MUSIC ENABLED"
@@ -138,3 +141,5 @@ game:
cheat_jail_open: "JAIL IS OPEN" cheat_jail_open: "JAIL IS OPEN"
debug_enabled: "DEBUG ENABLED" debug_enabled: "DEBUG ENABLED"
debug_disabled: "DEBUG DISABLED" debug_disabled: "DEBUG DISABLED"
editor_enabled: "EDITOR ENABLED"
editor_disabled: "EDITOR DISABLED"

View File

@@ -0,0 +1,19 @@
JASC-PAL
0100
16
52 49 40
80 73 57
92 104 82
108 116 76
125 130 73
163 158 85
202 181 103
119 63 53
132 86 64
160 119 84
188 153 120
214 193 157
234 220 193
247 240 221
255 251 237
255 255 255

View File

@@ -0,0 +1,19 @@
JASC-PAL
0100
16
32 32 32
45 33 30
69 41 35
109 61 41
177 107 74
232 159 110
232 190 130
93 117 87
142 146 87
112 123 136
138 167 172
229 93 77
241 134 108
210 103 48
222 154 40
232 216 165

View File

@@ -0,0 +1,19 @@
JASC-PAL
0100
16
22 23 26
127 6 34
214 36 17
255 132 38
255 209 0
250 253 255
255 128 164
255 38 116
148 33 106
67 0 103
35 73 117
104 174 212
191 255 60
16 210 117
0 120 153
0 40 89

19
data/palette/darkseed.pal Normal file
View File

@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0
0 20 24
0 32 36
0 44 56
20 52 68
68 52 68
88 60 72
108 76 68
128 96 88
108 112 108
136 128 120
164 148 132
196 172 156
216 176 168
236 212 208
252 252 252

19
data/palette/h16da.pal Normal file
View File

@@ -0,0 +1,19 @@
JASC-PAL
0100
16
226 217 228
108 154 154
82 103 93
55 64 59
243 200 147
229 152 125
203 94 92
114 51 76
192 165 169
191 125 133
128 77 83
64 48 56
124 143 178
76 82 116
46 51 77
31 32 37

View File

@@ -0,0 +1,19 @@
JASC-PAL
0100
16
10 8 25
49 50 67
69 59 70
87 84 117
130 105 128
164 111 114
185 115 113
205 95 105
229 76 81
201 55 73
144 161 168
140 147 137
195 150 145
236 151 134
235 171 145
219 182 167

View File

@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 3 60
0 82 96
0 157 74
10 255 82
0 56 132
0 138 197
0 247 255
255 92 255
172 41 206
96 0 136
177 5 133
255 0 78
42 46 121
78 110 168
173 212 250
255 255 255

View File

@@ -1,19 +0,0 @@
JASC-PAL
0100
16
26 28 44
93 39 93
177 62 83
239 125 87
255 205 117
167 240 112
56 183 100
37 113 121
41 54 111
59 93 201
65 166 246
115 239 247
244 244 244
148 176 194
86 108 134
51 60 87

View File

@@ -2,18 +2,18 @@ JASC-PAL
0100 0100
16 16
26 28 44 26 28 44
41 54 111
51 60 87
86 108 134
59 93 201
37 113 121
93 39 93 93 39 93
177 62 83 177 62 83
56 183 100
167 240 112
65 166 246
115 239 247
239 125 87 239 125 87
255 205 117 255 205 117
148 176 194 167 240 112
56 183 100
37 113 121
41 54 111
59 93 201
65 166 246
115 239 247
244 244 244 244 244 244
148 176 194
86 108 134
51 60 87

View File

@@ -0,0 +1,19 @@
JASC-PAL
0100
16
40 40 46
108 86 113
217 200 191
249 130 132
176 169 228
172 204 228
179 227 218
254 170 228
135 168 137
176 235 147
233 245 157
255 230 198
222 163 139
255 195 132
255 247 160
255 247 228

View File

@@ -0,0 +1,19 @@
JASC-PAL
0100
16
0 0 0
50 1 50
84 1 103
118 50 118
50 50 171
35 103 239
152 186 220
253 253 253
1 186 152
1 137 84
254 239 69
119 70 2
186 118 84
254 1 69
239 152 152
254 205 205

View File

@@ -4,17 +4,7 @@ frameWidth: 8
frameHeight: 16 frameHeight: 16
animations: animations:
- name: stand - name: default
speed: 0.1333
loop: 0
frames: [0]
- name: walk
speed: 0.1333 speed: 0.1333
loop: 0 loop: 0
frames: [0, 1, 2, 3] frames: [0, 1, 2, 3]
- name: walk_menu
speed: 0.0
loop: 0
frames: [0, 1, 2, 3]

View File

@@ -4,17 +4,7 @@ frameWidth: 8
frameHeight: 16 frameHeight: 16
animations: animations:
- name: stand - name: default
speed: 0.1333
loop: 0
frames: [0]
- name: walk
speed: 0.1333 speed: 0.1333
loop: 0 loop: 0
frames: [0, 1, 2, 3, 4, 5, 6, 7] frames: [0, 1, 2, 3, 4, 5, 6, 7]
- name: walk_menu
speed: 0.0
loop: 0
frames: [0, 1, 2, 3, 4, 5, 6, 7]

View File

@@ -92,21 +92,21 @@ namespace GlobalInputs {
void handleToggleShaders() { void handleToggleShaders() {
Screen::get()->toggleShaders(); Screen::get()->toggleShaders();
Notifier::get()->show({Locale::get()->get(Options::video.postfx ? "ui.shaders_enabled" : "ui.shaders_disabled")}); // NOLINT(readability-static-accessed-through-instance) Notifier::get()->show({Locale::get()->get(Options::video.shader.enabled ? "ui.shaders_enabled" : "ui.shaders_disabled")}); // NOLINT(readability-static-accessed-through-instance)
} }
void handleNextShaderPreset() { void handleNextShaderPreset() {
if (Options::current_shader == Rendering::ShaderType::CRTPI) { if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
if (!Options::crtpi_presets.empty()) { if (!Options::crtpi_presets.empty()) {
Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size()); Options::video.shader.current_crtpi_preset = (Options::video.shader.current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
Screen::get()->reloadCrtPi(); Screen::get()->reloadCrtPi();
Notifier::get()->show({Locale::get()->get("ui.crtpi") + " " + Options::crtpi_presets[static_cast<size_t>(Options::current_crtpi_preset)].name}); // NOLINT(readability-static-accessed-through-instance) Notifier::get()->show({Locale::get()->get("ui.crtpi") + " " + prettyName(Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)].name)}); // NOLINT(readability-static-accessed-through-instance)
} }
} else { } else {
if (!Options::postfx_presets.empty()) { if (!Options::postfx_presets.empty()) {
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size()); Options::video.shader.current_postfx_preset = (Options::video.shader.current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
Screen::get()->reloadPostFX(); Screen::get()->reloadPostFX();
Notifier::get()->show({Locale::get()->get("ui.postfx") + " " + Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)].name}); // NOLINT(readability-static-accessed-through-instance) Notifier::get()->show({Locale::get()->get("ui.postfx") + " " + prettyName(Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)].name)}); // NOLINT(readability-static-accessed-through-instance)
} }
} }
} }
@@ -114,17 +114,22 @@ namespace GlobalInputs {
void handleNextShader() { void handleNextShader() {
Screen::get()->nextShader(); Screen::get()->nextShader();
Notifier::get()->show({Locale::get()->get("ui.shader") + " " + // NOLINT(readability-static-accessed-through-instance) Notifier::get()->show({Locale::get()->get("ui.shader") + " " + // NOLINT(readability-static-accessed-through-instance)
(Options::current_shader == Rendering::ShaderType::CRTPI ? "CRTPI" : "POSTFX")}); (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI ? "CRTPI" : "POSTFX")});
} }
void handleNextPalette() { void handleNextPalette() {
Screen::get()->nextPalette(); Screen::get()->nextPalette();
Notifier::get()->show({Locale::get()->get("ui.palette") + " " + Options::video.palette}); // NOLINT(readability-static-accessed-through-instance) Notifier::get()->show({Locale::get()->get("ui.palette") + " " + toUpper(Screen::get()->getPalettePrettyName())}); // NOLINT(readability-static-accessed-through-instance)
} }
void handlePreviousPalette() { void handlePreviousPalette() {
Screen::get()->previousPalette(); Screen::get()->previousPalette();
Notifier::get()->show({Locale::get()->get("ui.palette") + " " + Options::video.palette}); // NOLINT(readability-static-accessed-through-instance) Notifier::get()->show({Locale::get()->get("ui.palette") + " " + toUpper(Screen::get()->getPalettePrettyName())}); // NOLINT(readability-static-accessed-through-instance)
}
void handleNextPaletteSortMode() {
Screen::get()->nextPaletteSortMode();
Notifier::get()->show({Locale::get()->get("ui.palette_sort") + " " + toUpper(Screen::get()->getPaletteSortModeName())}); // NOLINT(readability-static-accessed-through-instance)
} }
void handleToggleIntegerScale() { void handleToggleIntegerScale() {
@@ -160,20 +165,25 @@ namespace GlobalInputs {
return InputAction::WINDOW_INC_ZOOM; return InputAction::WINDOW_INC_ZOOM;
} }
} }
if (Input::get()->checkAction(InputAction::TOGGLE_SHADER, Input::DO_NOT_ALLOW_REPEAT)) { if (Screen::get()->isHardwareAccelerated()) {
if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) { if (Input::get()->checkAction(InputAction::TOGGLE_SHADER, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::TOGGLE_SUPERSAMPLING; // Ctrl+F4 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 (Options::video.postfx && ((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 (Input::get()->checkAction(InputAction::NEXT_PALETTE, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::NEXT_PALETTE; if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) {
return InputAction::PREVIOUS_PALETTE; // Ctrl+F5
}
return InputAction::NEXT_PALETTE; // F5
} }
if (Input::get()->checkAction(InputAction::PREVIOUS_PALETTE, Input::DO_NOT_ALLOW_REPEAT)) { if (Input::get()->checkAction(InputAction::NEXT_PALETTE_SORT, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::PREVIOUS_PALETTE; return InputAction::NEXT_PALETTE_SORT; // F6
} }
if (Input::get()->checkAction(InputAction::TOGGLE_INTEGER_SCALE, Input::DO_NOT_ALLOW_REPEAT)) { if (Input::get()->checkAction(InputAction::TOGGLE_INTEGER_SCALE, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::TOGGLE_INTEGER_SCALE; return InputAction::TOGGLE_INTEGER_SCALE;
@@ -271,6 +281,10 @@ namespace GlobalInputs {
handlePreviousPalette(); handlePreviousPalette();
break; break;
case InputAction::NEXT_PALETTE_SORT:
handleNextPaletteSortMode();
break;
case InputAction::TOGGLE_INTEGER_SCALE: case InputAction::TOGGLE_INTEGER_SCALE:
handleToggleIntegerScale(); handleToggleIntegerScale();
break; break;

View File

@@ -45,7 +45,7 @@ Input::Input(std::string game_controller_db_path)
{Action::TOGGLE_FULLSCREEN, KeyState{.scancode = SDL_SCANCODE_F3}}, {Action::TOGGLE_FULLSCREEN, KeyState{.scancode = SDL_SCANCODE_F3}},
{Action::TOGGLE_SHADER, KeyState{.scancode = SDL_SCANCODE_F4}}, {Action::TOGGLE_SHADER, KeyState{.scancode = SDL_SCANCODE_F4}},
{Action::NEXT_PALETTE, KeyState{.scancode = SDL_SCANCODE_F5}}, {Action::NEXT_PALETTE, KeyState{.scancode = SDL_SCANCODE_F5}},
{Action::PREVIOUS_PALETTE, KeyState{.scancode = SDL_SCANCODE_F6}}, {Action::NEXT_PALETTE_SORT, KeyState{.scancode = SDL_SCANCODE_F6}},
{Action::TOGGLE_INTEGER_SCALE, KeyState{.scancode = SDL_SCANCODE_F7}}, {Action::TOGGLE_INTEGER_SCALE, KeyState{.scancode = SDL_SCANCODE_F7}},
{Action::TOGGLE_IN_GAME_MUSIC, KeyState{.scancode = SDL_SCANCODE_F8}}, {Action::TOGGLE_IN_GAME_MUSIC, KeyState{.scancode = SDL_SCANCODE_F8}},
{Action::TOGGLE_BORDER, KeyState{.scancode = SDL_SCANCODE_F9}}, {Action::TOGGLE_BORDER, KeyState{.scancode = SDL_SCANCODE_F9}},

View File

@@ -20,6 +20,7 @@ const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
{InputAction::TOGGLE_IN_GAME_MUSIC, "TOGGLE_MUSIC"}, {InputAction::TOGGLE_IN_GAME_MUSIC, "TOGGLE_MUSIC"},
{InputAction::NEXT_PALETTE, "NEXT_PALETTE"}, {InputAction::NEXT_PALETTE, "NEXT_PALETTE"},
{InputAction::PREVIOUS_PALETTE, "PREVIOUS_PALETTE"}, {InputAction::PREVIOUS_PALETTE, "PREVIOUS_PALETTE"},
{InputAction::NEXT_PALETTE_SORT, "NEXT_PALETTE_SORT"},
{InputAction::TOGGLE_SHADER, "TOGGLE_POSTFX"}, {InputAction::TOGGLE_SHADER, "TOGGLE_POSTFX"},
{InputAction::NEXT_SHADER_PRESET, "NEXT_POSTFX_PRESET"}, {InputAction::NEXT_SHADER_PRESET, "NEXT_POSTFX_PRESET"},
{InputAction::TOGGLE_INFO, "TOGGLE_DEBUG"}, {InputAction::TOGGLE_INFO, "TOGGLE_DEBUG"},
@@ -42,6 +43,7 @@ const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
{"TOGGLE_MUSIC", InputAction::TOGGLE_IN_GAME_MUSIC}, {"TOGGLE_MUSIC", InputAction::TOGGLE_IN_GAME_MUSIC},
{"NEXT_PALETTE", InputAction::NEXT_PALETTE}, {"NEXT_PALETTE", InputAction::NEXT_PALETTE},
{"PREVIOUS_PALETTE", InputAction::PREVIOUS_PALETTE}, {"PREVIOUS_PALETTE", InputAction::PREVIOUS_PALETTE},
{"NEXT_PALETTE_SORT", InputAction::NEXT_PALETTE_SORT},
{"TOGGLE_POSTFX", InputAction::TOGGLE_SHADER}, {"TOGGLE_POSTFX", InputAction::TOGGLE_SHADER},
{"NEXT_POSTFX_PRESET", InputAction::NEXT_SHADER_PRESET}, {"NEXT_POSTFX_PRESET", InputAction::NEXT_SHADER_PRESET},
{"TOGGLE_DEBUG", InputAction::TOGGLE_INFO}, {"TOGGLE_DEBUG", InputAction::TOGGLE_INFO},

View File

@@ -31,6 +31,7 @@ enum class InputAction : int { // Acciones de entrada posibles en el juego
TOGGLE_IN_GAME_MUSIC, TOGGLE_IN_GAME_MUSIC,
NEXT_PALETTE, NEXT_PALETTE,
PREVIOUS_PALETTE, PREVIOUS_PALETTE,
NEXT_PALETTE_SORT,
TOGGLE_INFO, TOGGLE_INFO,
TOGGLE_CONSOLE, TOGGLE_CONSOLE,

View File

@@ -2,20 +2,146 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <cmath>
#include <string> #include <string>
#include <vector>
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "game/defaults.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "utils/utils.hpp" #include "utils/utils.hpp"
// ── Conversión string ↔ PaletteSortMode ──────────────────────────────────────
auto sortModeFromString(const std::string& str) -> PaletteSortMode {
const std::string lower = toLower(str);
if (lower == "luminance") { return PaletteSortMode::LUMINANCE; }
if (lower == "spectrum") { return PaletteSortMode::SPECTRUM; }
return PaletteSortMode::ORIGINAL;
}
auto sortModeToString(PaletteSortMode mode) -> std::string {
switch (mode) {
case PaletteSortMode::LUMINANCE:
return "luminance";
case PaletteSortMode::SPECTRUM:
return "spectrum";
default:
return "original";
}
}
// ── Paleta de referencia ZX Spectrum (16 colores ARGB) ───────────────────────
namespace {
// Helpers para extraer componentes RGB de un color ARGB (0xAARRGGBB)
constexpr auto redOf(Uint32 c) -> int { return static_cast<int>((c >> 16) & 0xFF); }
constexpr auto greenOf(Uint32 c) -> int { return static_cast<int>((c >> 8) & 0xFF); }
constexpr auto blueOf(Uint32 c) -> int { return static_cast<int>(c & 0xFF); }
constexpr auto makeARGB(int r, int g, int b) -> Uint32 {
return (0xFFU << 24) | (static_cast<Uint32>(r) << 16) | (static_cast<Uint32>(g) << 8) | static_cast<Uint32>(b);
}
// Paleta ZX Spectrum de referencia (misma que en tools/sort_palette/sort_palette.py)
constexpr std::array<Uint32, 16> SPECTRUM_REFERENCE = {
makeARGB(0, 0, 0),
makeARGB(0, 0, 0),
makeARGB(0, 0, 216),
makeARGB(0, 0, 255),
makeARGB(216, 0, 0),
makeARGB(255, 0, 0),
makeARGB(216, 0, 216),
makeARGB(255, 0, 255),
makeARGB(0, 216, 0),
makeARGB(0, 255, 0),
makeARGB(0, 216, 216),
makeARGB(0, 255, 255),
makeARGB(216, 216, 0),
makeARGB(255, 255, 0),
makeARGB(216, 216, 216),
makeARGB(255, 255, 255),
};
// Luminancia percibida (ITU-R BT.709)
auto luminance(Uint32 color) -> double {
return 0.2126 * redOf(color) + 0.7152 * greenOf(color) + 0.0722 * blueOf(color);
}
// Distancia euclídea al cuadrado en espacio RGB (no necesita sqrt para comparar)
auto rgbDistanceSq(Uint32 a, Uint32 b) -> int {
const int dr = redOf(a) - redOf(b);
const int dg = greenOf(a) - greenOf(b);
const int db = blueOf(a) - blueOf(b);
return dr * dr + dg * dg + db * db;
}
// Cuenta los colores activos en la paleta (los que tienen alpha != 0)
auto countActiveColors(const Palette& palette) -> size_t {
size_t count = 0;
for (const auto& c : palette) {
if (c == 0) { break; }
++count;
}
return count;
}
// Ordenar por luminancia
auto sortByLuminance(const Palette& palette) -> Palette {
const size_t n = countActiveColors(palette);
std::vector<Uint32> colors(palette.begin(), palette.begin() + static_cast<ptrdiff_t>(n));
std::sort(colors.begin(), colors.end(), [](Uint32 a, Uint32 b) {
return luminance(a) < luminance(b);
});
Palette result{};
result.fill(0);
std::copy(colors.begin(), colors.end(), result.begin());
return result;
}
// Ordenar por similitud con la paleta ZX Spectrum (greedy matching)
auto sortBySpectrum(const Palette& palette) -> Palette {
const size_t n = countActiveColors(palette);
std::vector<Uint32> available(palette.begin(), palette.begin() + static_cast<ptrdiff_t>(n));
std::vector<Uint32> result;
result.reserve(n);
// Para cada color de referencia del Spectrum, buscar el más cercano disponible
const size_t refs = std::min(n, SPECTRUM_REFERENCE.size());
for (size_t i = 0; i < refs && !available.empty(); ++i) {
const Uint32 ref = SPECTRUM_REFERENCE[i];
auto best = std::min_element(available.begin(), available.end(), [ref](Uint32 a, Uint32 b) {
return rgbDistanceSq(a, ref) < rgbDistanceSq(b, ref);
});
result.push_back(*best);
available.erase(best);
}
// Si quedan colores sin asignar, añadirlos al final
for (const auto& c : available) {
result.push_back(c);
}
Palette out{};
out.fill(0);
std::copy(result.begin(), result.end(), out.begin());
return out;
}
} // namespace
// ── PaletteManager ───────────────────────────────────────────────────────────
PaletteManager::PaletteManager( PaletteManager::PaletteManager(
std::vector<std::string> raw_paths, std::vector<std::string> raw_paths,
const std::string& initial_name, const std::string& initial_name,
PaletteSortMode initial_sort_mode,
std::shared_ptr<Surface> game_surface, std::shared_ptr<Surface> game_surface,
std::shared_ptr<Surface> border_surface, std::shared_ptr<Surface> border_surface,
OnChangeCallback on_change) OnChangeCallback on_change)
: palettes_(std::move(raw_paths)), : palettes_(std::move(raw_paths)),
sort_mode_(initial_sort_mode),
game_surface_(std::move(game_surface)), game_surface_(std::move(game_surface)),
border_surface_(std::move(border_surface)), border_surface_(std::move(border_surface)),
on_change_(std::move(on_change)) { on_change_(std::move(on_change)) {
@@ -23,7 +149,7 @@ PaletteManager::PaletteManager(
// Leer y aplicar paleta inicial directamente desde el archivo // Leer y aplicar paleta inicial directamente desde el archivo
// (Resource::Cache aún no está disponible en este punto del ciclo de vida) // (Resource::Cache aún no está disponible en este punto del ciclo de vida)
const auto initial_palette = readPalFile(palettes_.at(current_)); const auto initial_palette = sortPalette(readPalFile(palettes_.at(current_)), sort_mode_);
game_surface_->setPalette(initial_palette); game_surface_->setPalette(initial_palette);
border_surface_->setPalette(initial_palette); border_surface_->setPalette(initial_palette);
@@ -44,9 +170,9 @@ void PaletteManager::previous() {
} }
auto PaletteManager::setByName(const std::string& name) -> bool { auto PaletteManager::setByName(const std::string& name) -> bool {
const std::string upper_name = toUpper(name + ".pal"); const std::string lower_name = toLower(name + ".pal");
for (size_t i = 0; i < palettes_.size(); ++i) { for (size_t i = 0; i < palettes_.size(); ++i) {
if (toUpper(palettes_[i]) == upper_name) { if (toLower(palettes_[i]) == lower_name) {
current_ = i; current_ = i;
apply(); apply();
return true; return true;
@@ -62,7 +188,7 @@ auto PaletteManager::getNames() const -> std::vector<std::string> {
std::string name = p; std::string name = p;
const size_t pos = name.find(".pal"); const size_t pos = name.find(".pal");
if (pos != std::string::npos) { name.erase(pos, 4); } if (pos != std::string::npos) { name.erase(pos, 4); }
std::ranges::transform(name, name.begin(), ::toupper); std::ranges::transform(name, name.begin(), ::tolower);
names.push_back(std::move(name)); names.push_back(std::move(name));
} }
return names; return names;
@@ -72,13 +198,41 @@ auto PaletteManager::getCurrentName() const -> std::string {
std::string name = palettes_.at(current_); std::string name = palettes_.at(current_);
const size_t pos = name.find(".pal"); const size_t pos = name.find(".pal");
if (pos != std::string::npos) { name.erase(pos, 4); } if (pos != std::string::npos) { name.erase(pos, 4); }
std::ranges::transform(name, name.begin(), ::toupper); std::ranges::transform(name, name.begin(), ::tolower);
return name; return name;
} }
auto PaletteManager::getPrettyName() const -> std::string {
std::string name = getCurrentName();
std::ranges::replace(name, '-', ' ');
return name;
}
void PaletteManager::nextSortMode() {
sort_mode_ = static_cast<PaletteSortMode>((static_cast<int>(sort_mode_) + 1) % static_cast<int>(PaletteSortMode::COUNT));
Options::video.palette_sort = sortModeToString(sort_mode_);
apply();
}
void PaletteManager::setSortMode(PaletteSortMode mode) {
sort_mode_ = mode;
Options::video.palette_sort = sortModeToString(sort_mode_);
apply();
}
auto PaletteManager::getSortMode() const -> PaletteSortMode {
return sort_mode_;
}
auto PaletteManager::getSortModeName() const -> std::string {
return sortModeToString(sort_mode_);
}
void PaletteManager::apply() { void PaletteManager::apply() {
game_surface_->loadPalette(Resource::Cache::get()->getPalette(palettes_.at(current_))); Palette raw = Resource::Cache::get()->getPalette(palettes_.at(current_));
border_surface_->loadPalette(Resource::Cache::get()->getPalette(palettes_.at(current_))); Palette sorted = sortPalette(raw, sort_mode_);
game_surface_->loadPalette(sorted);
border_surface_->loadPalette(sorted);
Options::video.palette = getCurrentName(); Options::video.palette = getCurrentName();
@@ -88,9 +242,16 @@ void PaletteManager::apply() {
} }
auto PaletteManager::findIndex(const std::string& name) const -> size_t { auto PaletteManager::findIndex(const std::string& name) const -> size_t {
const std::string upper_name = toUpper(name + ".pal"); const std::string lower_name = toLower(name + ".pal");
for (size_t i = 0; i < palettes_.size(); ++i) { for (size_t i = 0; i < palettes_.size(); ++i) {
if (toUpper(getFileName(palettes_[i])) == upper_name) { if (toLower(getFileName(palettes_[i])) == lower_name) {
return i;
}
}
// Fallback: buscar la paleta por defecto
const std::string default_name = toLower(std::string(Defaults::Video::PALETTE_NAME) + ".pal");
for (size_t i = 0; i < palettes_.size(); ++i) {
if (toLower(getFileName(palettes_[i])) == default_name) {
return i; return i;
} }
} }
@@ -102,3 +263,14 @@ void PaletteManager::processPathList() {
palette = getFileName(palette); palette = getFileName(palette);
} }
} }
auto PaletteManager::sortPalette(const Palette& palette, PaletteSortMode mode) -> Palette {
switch (mode) {
case PaletteSortMode::LUMINANCE:
return sortByLuminance(palette);
case PaletteSortMode::SPECTRUM:
return sortBySpectrum(palette);
default:
return palette;
}
}

View File

@@ -1,12 +1,30 @@
#pragma once #pragma once
#include <SDL3/SDL.h>
#include <array>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
// Alias de paleta (igual que en surface.hpp; evita incluir todo el header)
using Palette = std::array<Uint32, 256>;
class Surface; class Surface;
// Modo de ordenación de paletas
enum class PaletteSortMode : int {
ORIGINAL = 0, // Paleta tal cual viene del fichero
LUMINANCE = 1, // Ordenada por luminancia percibida
SPECTRUM = 2, // Reordenada para imitar la paleta ZX Spectrum
COUNT = 3
};
// Conversión string ↔ PaletteSortMode
auto sortModeFromString(const std::string& str) -> PaletteSortMode;
auto sortModeToString(PaletteSortMode mode) -> std::string;
class PaletteManager { class PaletteManager {
public: public:
using OnChangeCallback = std::function<void()>; using OnChangeCallback = std::function<void()>;
@@ -14,6 +32,7 @@ class PaletteManager {
PaletteManager( PaletteManager(
std::vector<std::string> raw_paths, std::vector<std::string> raw_paths,
const std::string& initial_name, const std::string& initial_name,
PaletteSortMode initial_sort_mode,
std::shared_ptr<Surface> game_surface, std::shared_ptr<Surface> game_surface,
std::shared_ptr<Surface> border_surface, std::shared_ptr<Surface> border_surface,
OnChangeCallback on_change = nullptr); OnChangeCallback on_change = nullptr);
@@ -21,16 +40,24 @@ class PaletteManager {
void next(); // Avanza a la siguiente paleta void next(); // Avanza a la siguiente paleta
void previous(); // Retrocede a la paleta anterior void previous(); // Retrocede a la paleta anterior
auto setByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe auto setByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe
[[nodiscard]] auto getNames() const -> std::vector<std::string>; // Nombres disponibles (mayúsculas, sin .pal) [[nodiscard]] auto getNames() const -> std::vector<std::string>; // Nombres disponibles (minúsculas, sin .pal)
[[nodiscard]] auto getCurrentName() const -> std::string; // Nombre de la paleta actual (mayúsculas, sin .pal) [[nodiscard]] auto getCurrentName() const -> std::string; // Nombre de la paleta actual (minúsculas, sin .pal)
[[nodiscard]] auto getPrettyName() const -> std::string; // Nombre actual con guiones sustituidos por espacios
void nextSortMode(); // Cicla al siguiente modo de ordenación
void setSortMode(PaletteSortMode mode); // Establece un modo de ordenación concreto
[[nodiscard]] auto getSortMode() const -> PaletteSortMode; // Devuelve el modo de ordenación actual
[[nodiscard]] auto getSortModeName() const -> std::string; // Nombre del modo actual ("ORIGINAL", etc.)
private: private:
void apply(); // Aplica la paleta actual a ambas surfaces 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 [[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 void processPathList(); // Extrae nombres de archivo de las rutas completas
static auto sortPalette(const Palette& palette, PaletteSortMode mode) -> Palette; // Reordena una paleta según el modo
std::vector<std::string> palettes_; std::vector<std::string> palettes_;
size_t current_{0}; size_t current_{0};
PaletteSortMode sort_mode_{PaletteSortMode::ORIGINAL};
std::shared_ptr<Surface> game_surface_; std::shared_ptr<Surface> game_surface_;
std::shared_ptr<Surface> border_surface_; std::shared_ptr<Surface> border_surface_;
OnChangeCallback on_change_; OnChangeCallback on_change_;

View File

@@ -14,6 +14,7 @@
#include "game/options.hpp" // Para Options #include "game/options.hpp" // Para Options
#include "game/ui/console.hpp" // Para Console #include "game/ui/console.hpp" // Para Console
#include "game/ui/notifier.hpp" // Para Notifier #include "game/ui/notifier.hpp" // Para Notifier
#include "utils/utils.hpp" // Para prettyName
// [SINGLETON] // [SINGLETON]
RenderInfo* RenderInfo::render_info = nullptr; RenderInfo* RenderInfo::render_info = nullptr;
@@ -89,20 +90,20 @@ void RenderInfo::render() const {
line += " | " + zoom_str + "x"; line += " | " + zoom_str + "x";
// PostFX: muestra shader + preset y supersampling, o nada si está desactivado // PostFX: muestra shader + preset y supersampling, o nada si está desactivado
if (Options::video.postfx) { if (Options::video.shader.enabled) {
const bool IS_CRTPI = (Options::current_shader == Rendering::ShaderType::CRTPI); const bool IS_CRTPI = (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI);
const std::string SHADER_NAME = IS_CRTPI ? "crtpi" : "postfx"; const std::string SHADER_NAME = IS_CRTPI ? "crtpi" : "postfx";
std::string preset_name = "-"; std::string preset_name = "-";
if (IS_CRTPI) { if (IS_CRTPI) {
if (!Options::crtpi_presets.empty()) { if (!Options::crtpi_presets.empty()) {
preset_name = Options::crtpi_presets[static_cast<size_t>(Options::current_crtpi_preset)].name; preset_name = prettyName(Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)].name);
} }
} else { } else {
if (!Options::postfx_presets.empty()) { if (!Options::postfx_presets.empty()) {
preset_name = Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)].name; preset_name = prettyName(Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)].name);
} }
} }
const bool SHOW_SS = Options::video.supersampling && !IS_CRTPI; const bool SHOW_SS = Options::video.supersampling.enabled && !IS_CRTPI;
line += " | " + SHADER_NAME + " " + preset_name + (SHOW_SS ? " (ss)" : ""); line += " | " + SHADER_NAME + " " + preset_name + (SHOW_SS ? " (ss)" : "");
} }

View File

@@ -85,6 +85,7 @@ Screen::Screen() {
palette_manager_ = std::make_unique<PaletteManager>( palette_manager_ = std::make_unique<PaletteManager>(
Resource::List::get()->getListByType(Resource::List::Type::PALETTE), Resource::List::get()->getListByType(Resource::List::Type::PALETTE),
Options::video.palette, Options::video.palette,
sortModeFromString(Options::video.palette_sort),
game_surface_, game_surface_,
border_surface_, border_surface_,
[this]() { [this]() {
@@ -239,11 +240,11 @@ void Screen::renderNotifications() const {
// Activa/desactiva todos los shaders respetando el shader actualmente seleccionado // Activa/desactiva todos los shaders respetando el shader actualmente seleccionado
void Screen::toggleShaders() { void Screen::toggleShaders() {
Options::video.postfx = !Options::video.postfx; Options::video.shader.enabled = !Options::video.shader.enabled;
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) { if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
if (Options::video.postfx) { if (Options::video.shader.enabled) {
// Activar: usar el shader actualmente seleccionado // Activar: usar el shader actualmente seleccionado
if (Options::current_shader == Rendering::ShaderType::CRTPI) { if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI); shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
applyCurrentCrtPiPreset(); applyCurrentCrtPiPreset();
} else { } else {
@@ -262,10 +263,10 @@ void Screen::toggleShaders() {
// Recarga el shader del preset actual sin toggle // Recarga el shader del preset actual sin toggle
void Screen::reloadPostFX() { void Screen::reloadPostFX() {
if (Options::video.postfx && shader_backend_ && shader_backend_->isHardwareAccelerated()) { if (Options::video.shader.enabled && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
// El backend ya está activo: solo actualizar uniforms, sin recrear el pipeline // El backend ya está activo: solo actualizar uniforms, sin recrear el pipeline
applyCurrentPostFXPreset(); applyCurrentPostFXPreset();
} else if (Options::video.postfx) { } else if (Options::video.shader.enabled) {
initShaders(); initShaders();
} }
} }
@@ -446,8 +447,12 @@ void Screen::renderOverlays() {
// Cambia a una paleta por nombre (case-insensitive); devuelve false si no existe // Cambia a una paleta por nombre (case-insensitive); devuelve false si no existe
auto Screen::setPaletteByName(const std::string& name) -> bool { return palette_manager_->setByName(name); } auto Screen::setPaletteByName(const std::string& name) -> bool { return palette_manager_->setByName(name); }
// Devuelve los nombres de paletas disponibles (mayúsculas, sin extensión .pal) // Devuelve los nombres de paletas disponibles (minúsculas, sin extensión .pal)
auto Screen::getPaletteNames() const -> std::vector<std::string> { return palette_manager_->getNames(); } auto Screen::getPaletteNames() const -> std::vector<std::string> { return palette_manager_->getNames(); }
auto Screen::getPalettePrettyName() const -> std::string { return palette_manager_->getPrettyName(); }
void Screen::nextPaletteSortMode() { palette_manager_->nextSortMode(); }
void Screen::setPaletteSortMode(PaletteSortMode mode) { palette_manager_->setSortMode(mode); }
auto Screen::getPaletteSortModeName() const -> std::string { return palette_manager_->getSortModeName(); }
// Limpia la game_surface_ // Limpia la game_surface_
void Screen::clearSurface(Uint8 index) { game_surface_->clear(index); } void Screen::clearSurface(Uint8 index) { game_surface_->clear(index); }
@@ -504,14 +509,14 @@ auto loadData(const std::string& filepath) -> std::vector<uint8_t> {
} }
void Screen::setLinearUpscale(bool linear) { void Screen::setLinearUpscale(bool linear) {
Options::video.linear_upscale = linear; Options::video.supersampling.linear_upscale = linear;
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) { if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
shader_backend_->setLinearUpscale(linear); shader_backend_->setLinearUpscale(linear);
} }
} }
void Screen::setDownscaleAlgo(int algo) { void Screen::setDownscaleAlgo(int algo) {
Options::video.downscale_algo = algo; Options::video.supersampling.downscale_algo = algo;
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) { if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
shader_backend_->setDownscaleAlgo(algo); shader_backend_->setDownscaleAlgo(algo);
} }
@@ -524,8 +529,8 @@ auto Screen::getSsTextureSize() const -> std::pair<int, int> {
// Activa/desactiva el supersampling global (Ctrl+F4) // Activa/desactiva el supersampling global (Ctrl+F4)
void Screen::toggleSupersampling() { void Screen::toggleSupersampling() {
Options::video.supersampling = !Options::video.supersampling; Options::video.supersampling.enabled = !Options::video.supersampling.enabled;
if (Options::video.postfx && shader_backend_ && shader_backend_->isHardwareAccelerated()) { if (Options::video.shader.enabled && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
applyCurrentPostFXPreset(); applyCurrentPostFXPreset();
} }
} }
@@ -533,11 +538,11 @@ void Screen::toggleSupersampling() {
// Aplica los parámetros del preset actual al backend de shaders // Aplica los parámetros del preset actual al backend de shaders
void Screen::applyCurrentPostFXPreset() { // NOLINT(readability-convert-member-functions-to-static) void Screen::applyCurrentPostFXPreset() { // NOLINT(readability-convert-member-functions-to-static)
if (shader_backend_ && !Options::postfx_presets.empty()) { if (shader_backend_ && !Options::postfx_presets.empty()) {
const auto& p = Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)]; const auto& p = Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)];
// Supersampling es un toggle global (Options::video.supersampling), no por preset. // Supersampling es un toggle global (Options::video.supersampling.enabled), no por preset.
// setOversample primero: puede recrear texturas antes de que setPostFXParams // setOversample primero: puede recrear texturas antes de que setPostFXParams
// decida si hornear scanlines en CPU o aplicarlas en GPU. // decida si hornear scanlines en CPU o aplicarlas en GPU.
shader_backend_->setOversample(Options::video.supersampling ? 3 : 1); shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
Rendering::PostFXParams params{.vignette = p.vignette, .scanlines = p.scanlines, .chroma = p.chroma, .mask = p.mask, .gamma = p.gamma, .curvature = p.curvature, .bleeding = p.bleeding, .flicker = p.flicker}; Rendering::PostFXParams params{.vignette = p.vignette, .scanlines = p.scanlines, .chroma = p.chroma, .mask = p.mask, .gamma = p.gamma, .curvature = p.curvature, .bleeding = p.bleeding, .flicker = p.flicker};
shader_backend_->setPostFXParams(params); shader_backend_->setPostFXParams(params);
} }
@@ -546,7 +551,7 @@ void Screen::applyCurrentPostFXPreset() { // NOLINT(readability-convert-member-
// Aplica los parámetros del preset CrtPi actual al backend de shaders // Aplica los parámetros del preset CrtPi actual al backend de shaders
void Screen::applyCurrentCrtPiPreset() { // NOLINT(readability-convert-member-functions-to-static) void Screen::applyCurrentCrtPiPreset() { // NOLINT(readability-convert-member-functions-to-static)
if (shader_backend_ && !Options::crtpi_presets.empty()) { if (shader_backend_ && !Options::crtpi_presets.empty()) {
const auto& p = Options::crtpi_presets[static_cast<size_t>(Options::current_crtpi_preset)]; const auto& p = Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)];
Rendering::CrtPiParams params{ Rendering::CrtPiParams params{
.scanline_weight = p.scanline_weight, .scanline_weight = p.scanline_weight,
.scanline_gap_brightness = p.scanline_gap_brightness, .scanline_gap_brightness = p.scanline_gap_brightness,
@@ -569,9 +574,9 @@ void Screen::applyCurrentCrtPiPreset() { // NOLINT(readability-convert-member-f
// Cambia el shader de post-procesado activo y aplica el preset correspondiente // Cambia el shader de post-procesado activo y aplica el preset correspondiente
void Screen::setActiveShader(Rendering::ShaderType type) { void Screen::setActiveShader(Rendering::ShaderType type) {
Options::current_shader = type; Options::video.shader.current_shader = type;
if (!shader_backend_) { return; } if (!shader_backend_) { return; }
if (!Options::video.postfx) { if (!Options::video.shader.enabled) {
// Shaders desactivados: guardar preferencia pero mantener pass-through // Shaders desactivados: guardar preferencia pero mantener pass-through
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX); shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
shader_backend_->setPostFXParams(Rendering::PostFXParams{}); shader_backend_->setPostFXParams(Rendering::PostFXParams{});
@@ -587,7 +592,7 @@ void Screen::setActiveShader(Rendering::ShaderType type) {
// Cicla al siguiente shader disponible (preparado para futura UI) // Cicla al siguiente shader disponible (preparado para futura UI)
void Screen::nextShader() { void Screen::nextShader() {
const Rendering::ShaderType NEXT = (Options::current_shader == Rendering::ShaderType::POSTFX) const Rendering::ShaderType NEXT = (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX)
? Rendering::ShaderType::CRTPI ? Rendering::ShaderType::CRTPI
: Rendering::ShaderType::POSTFX; : Rendering::ShaderType::POSTFX;
setActiveShader(NEXT); setActiveShader(NEXT);
@@ -601,7 +606,8 @@ void Screen::initShaders() {
if (!shader_backend_) { if (!shader_backend_) {
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>(); shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
shader_backend_->setPreferredDriver(Options::video.gpu_preferred_driver); const std::string fallback_driver = "none";
shader_backend_->setPreferredDriver(Options::video.gpu.acceleration ? Options::video.gpu.preferred_driver : fallback_driver);
} }
shader_backend_->init(window_, tex, "", ""); shader_backend_->init(window_, tex, "", "");
gpu_driver_ = shader_backend_->getDriverName(); gpu_driver_ = shader_backend_->getDriverName();
@@ -609,10 +615,10 @@ void Screen::initShaders() {
// Propagar flags de vsync, integer scale, upscale y downscale al backend GPU // Propagar flags de vsync, integer scale, upscale y downscale al backend GPU
shader_backend_->setVSync(Options::video.vertical_sync); shader_backend_->setVSync(Options::video.vertical_sync);
shader_backend_->setScaleMode(Options::video.integer_scale); shader_backend_->setScaleMode(Options::video.integer_scale);
shader_backend_->setLinearUpscale(Options::video.linear_upscale); shader_backend_->setLinearUpscale(Options::video.supersampling.linear_upscale);
shader_backend_->setDownscaleAlgo(Options::video.downscale_algo); shader_backend_->setDownscaleAlgo(Options::video.supersampling.downscale_algo);
if (Options::video.postfx) { if (Options::video.shader.enabled) {
applyCurrentPostFXPreset(); applyCurrentPostFXPreset();
} else { } else {
// Pass-through: todos los efectos a 0, el shader solo copia la textura // Pass-through: todos los efectos a 0, el shader solo copia la textura
@@ -620,8 +626,8 @@ void Screen::initShaders() {
} }
// Restaurar el shader activo guardado en config (y sus parámetros CrtPi si aplica) // Restaurar el shader activo guardado en config (y sus parámetros CrtPi si aplica)
shader_backend_->setActiveShader(Options::current_shader); shader_backend_->setActiveShader(Options::video.shader.current_shader);
if (Options::current_shader == Rendering::ShaderType::CRTPI) { if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
applyCurrentCrtPiPreset(); applyCurrentCrtPiPreset();
} }
} }

View File

@@ -57,7 +57,11 @@ class Screen {
void nextPalette(); // Cambia a la siguiente paleta void nextPalette(); // Cambia a la siguiente paleta
void previousPalette(); // Cambia a la paleta anterior void previousPalette(); // Cambia a la paleta anterior
auto setPaletteByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe auto setPaletteByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe
[[nodiscard]] auto getPaletteNames() const -> std::vector<std::string>; // Nombres disponibles (mayúsculas, sin .pal) [[nodiscard]] auto getPaletteNames() const -> std::vector<std::string>; // Nombres disponibles (minúsculas, sin .pal)
[[nodiscard]] auto getPalettePrettyName() const -> std::string; // Nombre actual con guiones sustituidos por espacios
void nextPaletteSortMode(); // Cicla al siguiente modo de ordenación de paleta
void setPaletteSortMode(PaletteSortMode mode); // Establece modo de ordenación concreto
[[nodiscard]] auto getPaletteSortModeName() const -> std::string; // Nombre del modo de ordenación actual
void toggleShaders(); // Activa/desactiva todos los shaders respetando current_shader void toggleShaders(); // Activa/desactiva todos los shaders respetando current_shader
void toggleSupersampling(); // Activa/desactiva el supersampling global void toggleSupersampling(); // Activa/desactiva el supersampling global
void reloadPostFX(); // Recarga el shader del preset actual sin toggle void reloadPostFX(); // Recarga el shader del preset actual sin toggle
@@ -80,6 +84,7 @@ class Screen {
[[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; } [[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; }
[[nodiscard]] auto getGameSurfaceDstRect() const -> SDL_FRect { return game_surface_dstrect_; } [[nodiscard]] auto getGameSurfaceDstRect() const -> SDL_FRect { return game_surface_dstrect_; }
[[nodiscard]] auto getGPUDriver() const -> const std::string& { return gpu_driver_; } [[nodiscard]] auto getGPUDriver() const -> const std::string& { return gpu_driver_; }
[[nodiscard]] auto isHardwareAccelerated() const -> bool { return shader_backend_ && shader_backend_->isHardwareAccelerated(); }
[[nodiscard]] auto getLastFPS() const -> int { return fps_.last_value; } [[nodiscard]] auto getLastFPS() const -> int { return fps_.last_value; }
[[nodiscard]] auto getZoomFactor() const -> float { return zoom_factor_; } [[nodiscard]] auto getZoomFactor() const -> float { return zoom_factor_; }
[[nodiscard]] auto getMaxZoom() const -> int; [[nodiscard]] auto getMaxZoom() const -> int;

View File

@@ -403,7 +403,7 @@ namespace Rendering {
// ---------------------------------------------------------------- // ----------------------------------------------------------------
if (preferred_driver_ == "none") { if (preferred_driver_ == "none") {
SDL_Log("SDL3GPUShader: GPU disabled by config, using SDL_Renderer fallback"); SDL_Log("SDL3GPUShader: GPU disabled by config, using SDL_Renderer fallback");
driver_name_ = "none"; driver_name_ = ""; // vacío → RenderInfo mostrará "sdl"
return false; return false;
} }
if (device_ == nullptr) { if (device_ == nullptr) {

View File

@@ -47,14 +47,12 @@ class AnimatedSprite : public MovingSprite {
void setCurrentAnimation(int index = 0); // Establece la animación actual por índice void setCurrentAnimation(int index = 0); // Establece la animación actual por índice
void resetAnimation(); // Reinicia la animación void resetAnimation(); // Reinicia la animación
void setCurrentAnimationFrame(int num); // Establece el frame actual de la animación void setCurrentAnimationFrame(int num); // Establece el frame actual de la animación
void animate(float delta_time); // Calcula el frame correspondiente a la animación actual (time-based)
protected: protected:
// Constructor per a ús de subclasses que gestionen la surface directament (sense YAML) // Constructor per a ús de subclasses que gestionen la surface directament (sense YAML)
AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos); AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos);
// Métodos protegidos
void animate(float delta_time); // Calcula el frame correspondiente a la animación actual (time-based)
private: private:
// Variables miembro // Variables miembro
std::vector<AnimationData> animations_; // Vector con las diferentes animaciones std::vector<AnimationData> animations_; // Vector con las diferentes animaciones

View File

@@ -175,12 +175,12 @@ void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readabil
float x_end = std::min(rect->x + rect->w, surface_data_->width); float x_end = std::min(rect->x + rect->w, surface_data_->width);
float y_end = std::min(rect->y + rect->h, surface_data_->height); float y_end = std::min(rect->y + rect->h, surface_data_->height);
// Recorrer cada píxel dentro del rectángulo directamente // Rellenar fila a fila con memset (memoria contigua por fila)
for (int y = y_start; y < y_end; ++y) { Uint8* data_ptr = surface_data_->data.get();
for (int x = x_start; x < x_end; ++x) { const int surf_width = static_cast<int>(surface_data_->width);
const int INDEX = x + (y * surface_data_->width); const int row_width = static_cast<int>(x_end) - static_cast<int>(x_start);
surface_data_->data.get()[INDEX] = color; for (int y = static_cast<int>(y_start); y < static_cast<int>(y_end); ++y) {
} std::memset(data_ptr + (y * surf_width) + static_cast<int>(x_start), color, row_width);
} }
} }
@@ -192,16 +192,12 @@ void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(re
float x_end = std::min(rect->x + rect->w, surface_data_->width); float x_end = std::min(rect->x + rect->w, surface_data_->width);
float y_end = std::min(rect->y + rect->h, surface_data_->height); float y_end = std::min(rect->y + rect->h, surface_data_->height);
// Dibujar bordes horizontales // Dibujar bordes horizontales con memset (líneas contiguas en memoria)
for (int x = x_start; x < x_end; ++x) { Uint8* data_ptr = surface_data_->data.get();
// Borde superior const int surf_width = static_cast<int>(surface_data_->width);
const int TOP_INDEX = x + (y_start * surface_data_->width); const int row_width = static_cast<int>(x_end) - static_cast<int>(x_start);
surface_data_->data.get()[TOP_INDEX] = color; std::memset(data_ptr + (static_cast<int>(y_start) * surf_width) + static_cast<int>(x_start), color, row_width);
std::memset(data_ptr + ((static_cast<int>(y_end) - 1) * surf_width) + static_cast<int>(x_start), color, row_width);
// Borde inferior
const int BOTTOM_INDEX = x + ((y_end - 1) * surface_data_->width);
surface_data_->data.get()[BOTTOM_INDEX] = color;
}
// Dibujar bordes verticales // Dibujar bordes verticales
for (int y = y_start; y < y_end; ++y) { for (int y = y_start; y < y_end; ++y) {
@@ -261,6 +257,8 @@ void Surface::render(float dx, float dy, float sx, float sy, float w, float h) {
w = std::min(w, surface_data->width - dx); w = std::min(w, surface_data->width - dx);
h = std::min(h, surface_data->height - dy); h = std::min(h, surface_data->height - dy);
const Uint8* src_ptr = surface_data_->data.get();
Uint8* dst_ptr = surface_data->data.get();
for (int iy = 0; iy < h; ++iy) { for (int iy = 0; iy < h; ++iy) {
for (int ix = 0; ix < w; ++ix) { for (int ix = 0; ix < w; ++ix) {
// Verificar que las coordenadas de destino están dentro de los límites // Verificar que las coordenadas de destino están dentro de los límites
@@ -269,9 +267,9 @@ void Surface::render(float dx, float dy, float sx, float sy, float w, float h) {
int src_x = sx + ix; int src_x = sx + ix;
int src_y = sy + iy; int src_y = sy + iy;
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))]; Uint8 color = src_ptr[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
if (color != static_cast<Uint8>(transparent_color_)) { if (color != static_cast<Uint8>(transparent_color_)) {
surface_data->data.get()[static_cast<size_t>(dest_x + (dest_y * surface_data->width))] = sub_palette_[color]; dst_ptr[static_cast<size_t>(dest_x + (dest_y * surface_data->width))] = sub_palette_[color];
} }
} }
} }
@@ -299,6 +297,8 @@ void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { //
h = std::min(h, surface_data_dest->height - y); h = std::min(h, surface_data_dest->height - y);
// Renderiza píxel por píxel aplicando el flip si es necesario // Renderiza píxel por píxel aplicando el flip si es necesario
const Uint8* src_ptr = surface_data_->data.get();
Uint8* dst_ptr = surface_data_dest->data.get();
for (int iy = 0; iy < h; ++iy) { for (int iy = 0; iy < h; ++iy) {
for (int ix = 0; ix < w; ++ix) { for (int ix = 0; ix < w; ++ix) {
// Coordenadas de origen // Coordenadas de origen
@@ -312,9 +312,9 @@ void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { //
// Verificar que las coordenadas de destino están dentro de los límites // Verificar que las coordenadas de destino están dentro de los límites
if (dest_x >= 0 && dest_x < surface_data_dest->width && dest_y >= 0 && dest_y < surface_data_dest->height) { if (dest_x >= 0 && dest_x < surface_data_dest->width && dest_y >= 0 && dest_y < surface_data_dest->height) {
// Copia el píxel si no es transparente // Copia el píxel si no es transparente
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))]; Uint8 color = src_ptr[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
if (color != static_cast<Uint8>(transparent_color_)) { if (color != static_cast<Uint8>(transparent_color_)) {
surface_data_dest->data[dest_x + (dest_y * surface_data_dest->width)] = sub_palette_[color]; dst_ptr[static_cast<size_t>(dest_x + (dest_y * surface_data_dest->width))] = sub_palette_[color];
} }
} }
} }
@@ -567,13 +567,16 @@ void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { //
// Convertir `pitch` de bytes a Uint32 (asegurando alineación correcta en hardware) // Convertir `pitch` de bytes a Uint32 (asegurando alineación correcta en hardware)
int row_stride = pitch / sizeof(Uint32); int row_stride = pitch / sizeof(Uint32);
for (int y = 0; y < surface_data_->height; ++y) { // Cachear punteros fuera del bucle para permitir autovectorización SIMD
for (int x = 0; x < surface_data_->width; ++x) { const Uint8* src = surface_data_->data.get();
// Calcular la posición correcta en la textura teniendo en cuenta el stride const Uint32* pal = palette_.data();
int texture_index = (y * row_stride) + x; const int width = surface_data_->width;
int surface_index = (y * surface_data_->width) + x; const int height = surface_data_->height;
for (int y = 0; y < height; ++y) {
pixels[texture_index] = palette_[surface_data_->data.get()[surface_index]]; const Uint8* src_row = src + (y * width);
Uint32* dst_row = pixels + (y * row_stride);
for (int x = 0; x < width; ++x) {
dst_row[x] = pal[src_row[x]];
} }
} }
@@ -613,12 +616,16 @@ void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FR
int row_stride = pitch / sizeof(Uint32); int row_stride = pitch / sizeof(Uint32);
for (int y = 0; y < surface_data_->height; ++y) { // Cachear punteros fuera del bucle para permitir autovectorización SIMD
for (int x = 0; x < surface_data_->width; ++x) { const Uint8* src = surface_data_->data.get();
int texture_index = (y * row_stride) + x; const Uint32* pal = palette_.data();
int surface_index = (y * surface_data_->width) + x; const int width = surface_data_->width;
const int height = surface_data_->height;
pixels[texture_index] = palette_[surface_data_->data.get()[surface_index]]; for (int y = 0; y < height; ++y) {
const Uint8* src_row = src + (y * width);
Uint32* dst_row = pixels + (y * row_stride);
for (int x = 0; x < width; ++x) {
dst_row[x] = pal[src_row[x]];
} }
} }

View File

@@ -4,6 +4,7 @@
#include <algorithm> // Para find_if #include <algorithm> // Para find_if
#include <cstdlib> // Para exit, size_t #include <cstdlib> // Para exit, size_t
#include <fstream> // Para ifstream, istreambuf_iterator
#include <iostream> // Para basic_ostream, operator<<, endl, cout #include <iostream> // Para basic_ostream, operator<<, endl, cout
#include <stdexcept> // Para runtime_error #include <stdexcept> // Para runtime_error
#include <utility> #include <utility>
@@ -15,6 +16,7 @@
#include "core/resources/resource_list.hpp" // Para List, List::Type #include "core/resources/resource_list.hpp" // Para List, List::Type
#include "game/defaults.hpp" // Para Defaults namespace #include "game/defaults.hpp" // Para Defaults namespace
#include "game/gameplay/room.hpp" // Para RoomData, loadRoomFile, loadRoomTileFile #include "game/gameplay/room.hpp" // Para RoomData, loadRoomFile, loadRoomTileFile
#include "game/gameplay/room_loader.hpp" // Para RoomLoader::loadFromString
#include "game/options.hpp" // Para Options, OptionsGame, options #include "game/options.hpp" // Para Options, OptionsGame, options
#include "utils/defines.hpp" // Para WINDOW_CAPTION #include "utils/defines.hpp" // Para WINDOW_CAPTION
#include "utils/utils.hpp" // Para getFileName, printWithDots, PaletteColor #include "utils/utils.hpp" // Para getFileName, printWithDots, PaletteColor
@@ -173,6 +175,34 @@ namespace Resource {
throw std::runtime_error("Habitación no encontrada: " + name); throw std::runtime_error("Habitación no encontrada: " + name);
} }
#ifdef _DEBUG
// Recarga una habitación desde disco (para el editor de mapas)
// Lee directamente del filesystem (no del resource pack) para obtener los cambios del editor
void Cache::reloadRoom(const std::string& name) {
auto file_path = List::get()->get(name);
if (file_path.empty()) {
std::cerr << "reloadRoom: Cannot resolve path for " << name << '\n';
return;
}
// Leer directamente del filesystem (evita el resource pack que tiene datos antiguos)
std::ifstream file(file_path);
if (!file.is_open()) {
std::cerr << "reloadRoom: Cannot open " << file_path << '\n';
return;
}
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
// Parsear y actualizar el cache
auto it = std::ranges::find_if(rooms_, [&name](const auto& r) -> bool { return r.name == name; });
if (it != rooms_.end()) {
*(it->room) = RoomLoader::loadFromString(content, name);
std::cout << "reloadRoom: " << name << " reloaded from filesystem\n";
}
}
#endif
// Obtiene todas las habitaciones // Obtiene todas las habitaciones
auto Cache::getRooms() -> std::vector<RoomResource>& { auto Cache::getRooms() -> std::vector<RoomResource>& {
return rooms_; return rooms_;

View File

@@ -25,7 +25,10 @@ namespace Resource {
auto getRoom(const std::string& name) -> std::shared_ptr<Room::Data>; auto getRoom(const std::string& name) -> std::shared_ptr<Room::Data>;
auto getRooms() -> std::vector<RoomResource>&; auto getRooms() -> std::vector<RoomResource>&;
void reload(); // Recarga todos los recursos void reload(); // Recarga todos los recursos
#ifdef _DEBUG
void reloadRoom(const std::string& name); // Recarga una habitación desde disco
#endif
private: private:
// Estructura para llevar la cuenta de los recursos cargados // Estructura para llevar la cuenta de los recursos cargados

View File

@@ -36,7 +36,8 @@
#include "utils/defines.hpp" // Para WINDOW_CAPTION #include "utils/defines.hpp" // Para WINDOW_CAPTION
#ifdef _DEBUG #ifdef _DEBUG
#include "core/system/debug.hpp" // Para Debug #include "core/system/debug.hpp" // Para Debug
#include "game/editor/map_editor.hpp" // Para MapEditor
#endif #endif
#ifndef _WIN32 #ifndef _WIN32
@@ -183,6 +184,7 @@ Director::Director() {
Debug::get()->setDebugFile(Resource::List::get()->get("debug.yaml")); Debug::get()->setDebugFile(Resource::List::get()->get("debug.yaml"));
Debug::get()->loadFromFile(); Debug::get()->loadFromFile();
SceneManager::current = Debug::get()->getInitialScene(); SceneManager::current = Debug::get()->getInitialScene();
MapEditor::init();
#endif #endif
std::cout << "\n"; // Fin de inicialización de sistemas std::cout << "\n"; // Fin de inicialización de sistemas
@@ -217,6 +219,7 @@ Director::~Director() {
Cheevos::destroy(); Cheevos::destroy();
Locale::destroy(); Locale::destroy();
#ifdef _DEBUG #ifdef _DEBUG
MapEditor::destroy();
Debug::destroy(); Debug::destroy();
#endif #endif
Input::destroy(); Input::destroy();

View File

@@ -25,13 +25,15 @@ namespace Defaults::Video {
constexpr bool FULLSCREEN = false; // Modo de pantalla completa por defecto (false = ventana) constexpr bool FULLSCREEN = false; // Modo de pantalla completa por defecto (false = ventana)
constexpr Screen::Filter FILTER = Screen::Filter::NEAREST; // Filtro por defecto constexpr Screen::Filter FILTER = Screen::Filter::NEAREST; // Filtro por defecto
constexpr bool VERTICAL_SYNC = true; // Vsync activado por defecto constexpr bool VERTICAL_SYNC = true; // Vsync activado por defecto
constexpr bool POSTFX = false; // PostFX desactivado por defecto constexpr bool SHADER_ENABLED = false; // Shaders de post-procesado desactivados por defecto
constexpr bool SUPERSAMPLING = false; // Supersampling desactivado por defecto constexpr bool SUPERSAMPLING = false; // Supersampling desactivado por defecto
constexpr bool INTEGER_SCALE = true; // Escalado entero activado por defecto constexpr bool INTEGER_SCALE = true; // Escalado entero activado por defecto
constexpr bool KEEP_ASPECT = true; // Mantener aspecto activado por defecto constexpr bool KEEP_ASPECT = true; // Mantener aspecto activado por defecto
constexpr const char* PALETTE_NAME = "zx-spectrum"; // Paleta por defecto constexpr const char* PALETTE_NAME = "zx-spectrum"; // Paleta por defecto
constexpr const char* PALETTE_SORT = "original"; // Modo de ordenación de paleta por defecto
constexpr bool LINEAR_UPSCALE = false; // Upscale NEAREST por defecto constexpr bool LINEAR_UPSCALE = false; // Upscale NEAREST por defecto
constexpr int DOWNSCALE_ALGO = 1; // Downscale Lanczos2 por defecto constexpr int DOWNSCALE_ALGO = 1; // Downscale Lanczos2 por defecto
constexpr bool GPU_ACCELERATION = true; // Aceleración GPU activada por defecto
} // namespace Defaults::Video } // namespace Defaults::Video
namespace Defaults::Border { namespace Defaults::Border {
@@ -100,5 +102,6 @@ namespace Defaults::Game::Player {
constexpr int SPAWN_X = 25 * Tile::SIZE; // Posición X inicial constexpr int SPAWN_X = 25 * Tile::SIZE; // Posición X inicial
constexpr int SPAWN_Y = 13 * Tile::SIZE; // Posición Y inicial constexpr int SPAWN_Y = 13 * Tile::SIZE; // Posición Y inicial
constexpr SDL_FlipMode SPAWN_FLIP = Flip::LEFT; // Orientación inicial constexpr SDL_FlipMode SPAWN_FLIP = Flip::LEFT; // Orientación inicial
constexpr int SKIN = 1; // Skin del jugador por defecto (1=normal, 2=alternativa) constexpr const char* SKIN = "default"; // Skin del jugador por defecto
constexpr int COLOR = -1; // Color del jugador (-1 = automático según cheats)
} // namespace Defaults::Game::Player } // namespace Defaults::Game::Player

View File

@@ -0,0 +1,88 @@
#ifdef _DEBUG
#include "game/editor/editor_statusbar.hpp"
#include <string> // Para to_string
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/surface.hpp" // Para Surface
#include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource::Cache
#include "game/options.hpp" // Para Options::game
#include "utils/defines.hpp" // Para Tile::SIZE
#include "utils/utils.hpp" // Para stringToColor
// Constructor
EditorStatusBar::EditorStatusBar(const std::string& room_number, const std::string& room_name)
: room_number_(room_number),
room_name_(room_name) {
const float SURFACE_WIDTH = Options::game.width;
constexpr float SURFACE_HEIGHT = 6.0F * Tile::SIZE; // 48 pixels, igual que el scoreboard
surface_ = std::make_shared<Surface>(SURFACE_WIDTH, SURFACE_HEIGHT);
surface_dest_ = {.x = 0, .y = Options::game.height - SURFACE_HEIGHT, .w = SURFACE_WIDTH, .h = SURFACE_HEIGHT};
}
// Pinta la barra de estado en pantalla
void EditorStatusBar::render() {
surface_->render(nullptr, &surface_dest_);
}
// Actualiza la barra de estado
void EditorStatusBar::update([[maybe_unused]] float delta_time) {
fillTexture();
}
// Establece las coordenadas del ratón en tiles
void EditorStatusBar::setMouseTile(int tile_x, int tile_y) {
mouse_tile_x_ = tile_x;
mouse_tile_y_ = tile_y;
}
// Establece la información de la entidad seleccionada
void EditorStatusBar::setSelectionInfo(const std::string& info) {
selection_info_ = info;
}
// Establece el detalle adicional
void EditorStatusBar::setSelectionDetail(const std::string& detail) {
selection_detail_ = detail;
}
// Dibuja los elementos en la surface
void EditorStatusBar::fillTexture() {
auto previous_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(surface_);
surface_->clear(stringToColor("black"));
auto text = Resource::Cache::get()->getText("8bithud");
const Uint8 LABEL_COLOR = stringToColor("bright_cyan");
const Uint8 VALUE_COLOR = stringToColor("white");
// Línea 1: Número y nombre de la habitación
const std::string ROOM_TEXT = toLower(room_number_ + " " + room_name_);
text->writeColored(LEFT_X, LINE1_Y, ROOM_TEXT, LABEL_COLOR);
// Línea 2: Coordenadas del ratón en tiles
const std::string TILE_X_STR = (mouse_tile_x_ < 10 ? "0" : "") + std::to_string(mouse_tile_x_);
const std::string TILE_Y_STR = (mouse_tile_y_ < 10 ? "0" : "") + std::to_string(mouse_tile_y_);
text->writeColored(LEFT_X, LINE2_Y, toLower("tile:"), LABEL_COLOR);
text->writeColored(LEFT_X + 30, LINE2_Y, TILE_X_STR + "," + TILE_Y_STR, VALUE_COLOR);
// Línea 2 (continuación): info de selección o indicador de modo editor
if (!selection_info_.empty()) {
text->writeColored(LEFT_X + 60, LINE2_Y, toLower(selection_info_), stringToColor("bright_yellow"));
} else {
text->writeColored(176, LINE2_Y, toLower("editor"), stringToColor("bright_green"));
}
// Línea 3: detalle de la selección (propiedades del enemigo)
if (!selection_detail_.empty()) {
text->writeColored(LEFT_X, LINE3_Y, toLower(selection_detail_), stringToColor("bright_white"));
}
Screen::get()->setRendererSurface(previous_renderer);
}
#endif // _DEBUG

View File

@@ -0,0 +1,45 @@
#pragma once
#ifdef _DEBUG
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string
class Surface;
class EditorStatusBar {
public:
EditorStatusBar(const std::string& room_number, const std::string& room_name);
~EditorStatusBar() = default;
void render();
void update(float delta_time);
void setMouseTile(int tile_x, int tile_y);
void setSelectionInfo(const std::string& info);
void setSelectionDetail(const std::string& detail);
private:
void fillTexture(); // Dibuja los elementos en la surface
// Constantes de posición (en pixels dentro de la surface de 256x48)
static constexpr int LINE1_Y = 4; // Nombre de la habitación
static constexpr int LINE2_Y = 14; // Coordenadas del ratón + selección
static constexpr int LINE3_Y = 24; // Línea extra disponible
static constexpr int LEFT_X = 4; // Margen izquierdo
// Objetos
std::shared_ptr<Surface> surface_; // Surface donde dibujar la barra
SDL_FRect surface_dest_{}; // Rectángulo destino en pantalla
// Variables
std::string room_number_; // Número de la habitación
std::string room_name_; // Nombre de la habitación
int mouse_tile_x_{0}; // Coordenada X del ratón en tiles
int mouse_tile_y_{0}; // Coordenada Y del ratón en tiles
std::string selection_info_; // Información de la entidad seleccionada (línea 2)
std::string selection_detail_; // Detalle adicional (línea 3)
};
#endif // _DEBUG

View File

@@ -0,0 +1,768 @@
#ifdef _DEBUG
#include "game/editor/map_editor.hpp"
#include <SDL3/SDL.h>
#include <cmath> // Para std::round
#include <iostream> // Para cout
#include "core/input/mouse.hpp" // Para Mouse
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/surface.hpp" // Para Surface
#include "core/resources/resource_cache.hpp" // Para Resource::Cache
#include "core/resources/resource_list.hpp" // Para Resource::List
#include "game/editor/editor_statusbar.hpp" // Para EditorStatusBar
#include "game/ui/console.hpp" // Para Console
#include "game/editor/room_saver.hpp" // Para RoomSaver
#include "game/entities/player.hpp" // Para Player
#include "game/gameplay/enemy_manager.hpp" // Para EnemyManager
#include "game/gameplay/item_manager.hpp" // Para ItemManager
#include "game/gameplay/room.hpp" // Para Room
#include "game/options.hpp" // Para Options
#include "utils/defines.hpp" // Para Tile::SIZE, PlayArea
#include "utils/utils.hpp" // Para stringToColor
// Singleton
MapEditor* MapEditor::instance_ = nullptr;
void MapEditor::init() {
instance_ = new MapEditor();
}
void MapEditor::destroy() {
delete instance_;
instance_ = nullptr;
}
auto MapEditor::get() -> MapEditor* {
return instance_;
}
// Constructor
MapEditor::MapEditor() = default;
// Destructor
MapEditor::~MapEditor() = default;
// Entra en modo editor
void MapEditor::enter(std::shared_ptr<Room> room, std::shared_ptr<Player> player, const std::string& room_path, std::shared_ptr<Scoreboard::Data> scoreboard_data) {
if (active_) { return; }
room_ = std::move(room);
player_ = std::move(player);
room_path_ = room_path;
scoreboard_data_ = std::move(scoreboard_data);
// Cargar una copia de los datos de la habitación (para boundaries y edición)
auto room_data_ptr = Resource::Cache::get()->getRoom(room_path);
if (room_data_ptr) {
room_data_ = *room_data_ptr;
}
// Obtener la ruta completa y cargar el YAML original (para edición parcial y backup)
file_path_ = Resource::List::get()->get(room_path_);
if (!file_path_.empty()) {
yaml_ = RoomSaver::loadYAML(file_path_);
yaml_backup_ = yaml_; // Copia profunda para revert
}
// Guardar estado de invencibilidad y forzarla
invincible_before_editor_ = Options::cheats.invincible;
Options::cheats.invincible = Options::Cheat::State::ENABLED;
player_->setColor();
// Resetear enemigos a su posición inicial (pueden haberse movido durante el gameplay)
room_->resetEnemyPositions(room_data_.enemies);
// Crear la barra de estado
statusbar_ = std::make_unique<EditorStatusBar>(room_->getNumber(), room_->getName());
// Resetear estado
drag_ = {};
selected_enemy_ = -1;
active_ = true;
std::cout << "MapEditor: ON (room " << room_path_ << ")\n";
}
// Sale del modo editor
void MapEditor::exit() {
if (!active_) { return; }
active_ = false;
// Restaurar invencibilidad
Options::cheats.invincible = invincible_before_editor_;
player_->setColor();
// Restaurar prompt de la consola y limpiar estado
selected_enemy_ = -1;
Console::get()->setPrompt("> ");
drag_ = {};
statusbar_.reset();
room_.reset();
player_.reset();
scoreboard_data_.reset();
std::cout << "MapEditor: OFF\n";
}
// Revierte todos los cambios al estado original
auto MapEditor::revert() -> std::string {
if (!active_) { return "Editor not active"; }
if (file_path_.empty()) { return "Error: No file path"; }
// Restaurar el YAML original y reescribir el fichero
yaml_ = yaml_backup_;
auto room_data_ptr = Resource::Cache::get()->getRoom(room_path_);
if (room_data_ptr) {
room_data_ = *room_data_ptr;
}
RoomSaver::saveYAML(file_path_, yaml_, room_data_);
// Resetear los sprites vivos a las posiciones originales
room_->resetEnemyPositions(room_data_.enemies);
// Resetear items (recargar posiciones)
auto* item_mgr = room_->getItemManager();
for (int i = 0; i < item_mgr->getCount() && i < static_cast<int>(room_data_.items.size()); ++i) {
item_mgr->getItem(i)->setPosition(room_data_.items[i].x, room_data_.items[i].y);
}
return "Reverted to original";
}
// Auto-guarda al YAML tras soltar una entidad
void MapEditor::autosave() {
if (file_path_.empty()) { return; }
// Sincronizar posiciones de items desde los sprites vivos a room_data_
auto* item_mgr = room_->getItemManager();
for (int i = 0; i < item_mgr->getCount() && i < static_cast<int>(room_data_.items.size()); ++i) {
SDL_FPoint pos = item_mgr->getItem(i)->getPos();
room_data_.items[i].x = pos.x;
room_data_.items[i].y = pos.y;
}
RoomSaver::saveYAML(file_path_, yaml_, room_data_);
}
// Actualiza el editor
void MapEditor::update(float delta_time) {
// Mantener el ratón siempre visible
SDL_ShowCursor();
Mouse::last_mouse_move_time = SDL_GetTicks();
// Actualizar animaciones de enemigos e items (sin mover enemigos)
room_->updateEditorMode(delta_time);
// Actualizar posición del ratón
updateMousePosition();
// Si estamos arrastrando, actualizar la posición snapped
if (drag_.target != DragTarget::NONE) {
updateDrag();
}
// Actualizar la barra de estado
updateStatusBarInfo();
if (statusbar_) {
statusbar_->update(delta_time);
}
}
// Renderiza el editor
void MapEditor::render() {
// El tilemap ya ha sido renderizado por Game::renderPlaying() antes de llamar aquí
// Renderizar los marcadores de boundaries y líneas de ruta (debajo de los sprites)
renderEnemyBoundaries();
// Renderizar entidades normales: enemigos (animados en posición inicial), items, jugador
room_->renderEnemies();
room_->renderItems();
player_->render();
// Renderizar highlight de selección (encima de los sprites)
renderSelectionHighlight();
// Renderizar barra de estado del editor (reemplaza al scoreboard)
if (statusbar_) {
statusbar_->render();
}
}
// Maneja eventos del editor
void MapEditor::handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) {
handleMouseDown(mouse_game_x_, mouse_game_y_);
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT) {
handleMouseUp();
}
}
// Procesa click del ratón: hit test + inicio de drag
void MapEditor::handleMouseDown(float game_x, float game_y) {
// Prioridad de hit test: jugador → enemigos (initial) → enemigos (boundaries) → items
// Helper para iniciar drag
auto startDrag = [&](DragTarget target, int index, float entity_x, float entity_y) {
drag_.target = target;
drag_.index = index;
drag_.offset_x = game_x - entity_x;
drag_.offset_y = game_y - entity_y;
drag_.snap_x = entity_x;
drag_.snap_y = entity_y;
drag_.moved = false;
};
// 1. Hit test sobre el jugador (8x16)
SDL_FRect player_rect = player_->getRect();
if (pointInRect(game_x, game_y, player_rect)) {
startDrag(DragTarget::PLAYER, -1, player_rect.x, player_rect.y);
return;
}
// 2. Hit test sobre enemigos: posición inicial (usan el rect del sprite vivo)
auto* enemy_mgr = room_->getEnemyManager();
for (int i = 0; i < enemy_mgr->getCount(); ++i) {
SDL_FRect enemy_rect = enemy_mgr->getEnemy(i)->getRect();
if (pointInRect(game_x, game_y, enemy_rect)) {
startDrag(DragTarget::ENEMY_INITIAL, i, enemy_rect.x, enemy_rect.y);
return;
}
}
// 3. Hit test sobre boundaries de enemigos (rectángulos 8x8 en las posiciones de room_data_)
for (int i = 0; i < static_cast<int>(room_data_.enemies.size()); ++i) {
const auto& ed = room_data_.enemies[i];
constexpr float SZ = static_cast<float>(Tile::SIZE);
SDL_FRect b1_rect = {.x = static_cast<float>(ed.x1), .y = static_cast<float>(ed.y1), .w = SZ, .h = SZ};
if (pointInRect(game_x, game_y, b1_rect)) {
startDrag(DragTarget::ENEMY_BOUND1, i, b1_rect.x, b1_rect.y);
return;
}
SDL_FRect b2_rect = {.x = static_cast<float>(ed.x2), .y = static_cast<float>(ed.y2), .w = SZ, .h = SZ};
if (pointInRect(game_x, game_y, b2_rect)) {
startDrag(DragTarget::ENEMY_BOUND2, i, b2_rect.x, b2_rect.y);
return;
}
}
// 4. Hit test sobre items (8x8)
auto* item_mgr = room_->getItemManager();
for (int i = 0; i < item_mgr->getCount(); ++i) {
SDL_FRect item_rect = item_mgr->getItem(i)->getCollider();
if (pointInRect(game_x, game_y, item_rect)) {
startDrag(DragTarget::ITEM, i, item_rect.x, item_rect.y);
return;
}
}
// Click en el fondo: deseleccionar
selected_enemy_ = -1;
}
// Procesa soltar el ratón: commit del drag
void MapEditor::handleMouseUp() {
if (drag_.target == DragTarget::NONE) { return; }
const int IDX = drag_.index;
// Si no se movió: fue un click → seleccionar/deseleccionar
if (!drag_.moved) {
if (drag_.target == DragTarget::ENEMY_INITIAL) {
// Toggle selección: si ya estaba seleccionado, deseleccionar
selected_enemy_ = (selected_enemy_ == IDX) ? -1 : IDX;
} else {
// Click en otro tipo de entidad: deseleccionar enemigo
selected_enemy_ = -1;
}
drag_ = {};
return;
}
// Hubo movimiento: commit del drag
const int SNAP_X = static_cast<int>(drag_.snap_x);
const int SNAP_Y = static_cast<int>(drag_.snap_y);
bool changed = false;
switch (drag_.target) {
case DragTarget::PLAYER:
player_->setDebugPosition(drag_.snap_x, drag_.snap_y);
player_->finalizeDebugTeleport();
break;
case DragTarget::ENEMY_INITIAL:
if (IDX >= 0 && IDX < static_cast<int>(room_data_.enemies.size())) {
room_data_.enemies[IDX].x = drag_.snap_x;
room_data_.enemies[IDX].y = drag_.snap_y;
room_->getEnemyManager()->getEnemy(IDX)->resetToInitialPosition(room_data_.enemies[IDX]);
selected_enemy_ = IDX; // Seleccionar el enemigo arrastrado
changed = true;
}
break;
case DragTarget::ENEMY_BOUND1:
if (IDX >= 0 && IDX < static_cast<int>(room_data_.enemies.size())) {
room_data_.enemies[IDX].x1 = SNAP_X;
room_data_.enemies[IDX].y1 = SNAP_Y;
selected_enemy_ = IDX;
changed = true;
}
break;
case DragTarget::ENEMY_BOUND2:
if (IDX >= 0 && IDX < static_cast<int>(room_data_.enemies.size())) {
room_data_.enemies[IDX].x2 = SNAP_X;
room_data_.enemies[IDX].y2 = SNAP_Y;
selected_enemy_ = IDX;
changed = true;
}
break;
case DragTarget::ITEM:
if (IDX >= 0 && IDX < room_->getItemManager()->getCount()) {
room_->getItemManager()->getItem(IDX)->setPosition(drag_.snap_x, drag_.snap_y);
selected_enemy_ = -1;
changed = true;
}
break;
case DragTarget::NONE:
break;
}
if (changed) { autosave(); }
drag_ = {};
}
// Actualiza la posición snapped durante el drag
void MapEditor::updateDrag() {
float raw_x = mouse_game_x_ - drag_.offset_x;
float raw_y = mouse_game_y_ - drag_.offset_y;
float new_snap_x = snapToGrid(raw_x);
float new_snap_y = snapToGrid(raw_y);
// Detectar si hubo movimiento real (el snap cambió respecto al inicio)
if (new_snap_x != drag_.snap_x || new_snap_y != drag_.snap_y) {
drag_.moved = true;
}
drag_.snap_x = new_snap_x;
drag_.snap_y = new_snap_y;
// Mientras arrastramos, mover la entidad visualmente a la posición snapped
switch (drag_.target) {
case DragTarget::PLAYER:
player_->setDebugPosition(drag_.snap_x, drag_.snap_y);
break;
case DragTarget::ENEMY_INITIAL:
if (drag_.index >= 0 && drag_.index < room_->getEnemyManager()->getCount()) {
// Mover el sprite vivo del enemigo durante el arrastre
auto& enemy = room_->getEnemyManager()->getEnemy(drag_.index);
Enemy::Data temp_data = room_data_.enemies[drag_.index];
temp_data.x = drag_.snap_x;
temp_data.y = drag_.snap_y;
enemy->resetToInitialPosition(temp_data);
}
break;
case DragTarget::ENEMY_BOUND1:
// Los boundaries se actualizan visualmente en renderEnemyBoundaries() via drag_.snap
break;
case DragTarget::ENEMY_BOUND2:
break;
case DragTarget::ITEM:
if (drag_.index >= 0 && drag_.index < room_->getItemManager()->getCount()) {
room_->getItemManager()->getItem(drag_.index)->setPosition(drag_.snap_x, drag_.snap_y);
}
break;
case DragTarget::NONE:
break;
}
}
// Dibuja highlight del elemento seleccionado/arrastrado
void MapEditor::renderSelectionHighlight() {
auto game_surface = Screen::get()->getRendererSurface();
if (!game_surface) { return; }
constexpr float SZ = static_cast<float>(Tile::SIZE);
// Highlight del enemigo seleccionado (persistente, color bright_green)
if (selected_enemy_ >= 0 && selected_enemy_ < room_->getEnemyManager()->getCount()) {
SDL_FRect enemy_rect = room_->getEnemyManager()->getEnemy(selected_enemy_)->getRect();
SDL_FRect border = {
.x = enemy_rect.x - 1,
.y = enemy_rect.y - 1,
.w = enemy_rect.w + 2,
.h = enemy_rect.h + 2};
game_surface->drawRectBorder(&border, stringToColor("bright_green"));
}
// Highlight del drag activo (temporal, color bright_white)
if (drag_.target == DragTarget::NONE || !drag_.moved) { return; }
const Uint8 DRAG_COLOR = stringToColor("bright_white");
SDL_FRect highlight_rect{};
switch (drag_.target) {
case DragTarget::PLAYER:
highlight_rect = player_->getRect();
break;
case DragTarget::ENEMY_INITIAL:
if (drag_.index >= 0 && drag_.index < room_->getEnemyManager()->getCount()) {
highlight_rect = room_->getEnemyManager()->getEnemy(drag_.index)->getRect();
}
break;
case DragTarget::ENEMY_BOUND1:
case DragTarget::ENEMY_BOUND2:
highlight_rect = {.x = drag_.snap_x, .y = drag_.snap_y, .w = SZ, .h = SZ};
break;
case DragTarget::ITEM:
if (drag_.index >= 0 && drag_.index < room_->getItemManager()->getCount()) {
highlight_rect = room_->getItemManager()->getItem(drag_.index)->getCollider();
}
break;
case DragTarget::NONE:
return;
}
SDL_FRect border = {
.x = highlight_rect.x - 1,
.y = highlight_rect.y - 1,
.w = highlight_rect.w + 2,
.h = highlight_rect.h + 2};
game_surface->drawRectBorder(&border, DRAG_COLOR);
}
// Alinea un valor a la cuadrícula de 8x8
auto MapEditor::snapToGrid(float value) -> float {
return std::round(value / static_cast<float>(Tile::SIZE)) * static_cast<float>(Tile::SIZE);
}
// Hit test: punto dentro de rectángulo
auto MapEditor::pointInRect(float px, float py, const SDL_FRect& rect) -> bool {
return px >= rect.x && px < rect.x + rect.w && py >= rect.y && py < rect.y + rect.h;
}
// Dibuja marcadores de boundaries y líneas de ruta para los enemigos
void MapEditor::renderEnemyBoundaries() {
auto game_surface = Screen::get()->getRendererSurface();
if (!game_surface) { return; }
const Uint8 COLOR_BOUND1 = stringToColor("bright_cyan");
const Uint8 COLOR_BOUND2 = stringToColor("bright_yellow");
const Uint8 COLOR_ROUTE = stringToColor("bright_white");
for (int i = 0; i < static_cast<int>(room_data_.enemies.size()); ++i) {
const auto& enemy = room_data_.enemies[i];
constexpr float HALF = Tile::SIZE / 2.0F;
// Posiciones base (pueden estar siendo arrastradas)
float init_x = enemy.x;
float init_y = enemy.y;
float b1_x = static_cast<float>(enemy.x1);
float b1_y = static_cast<float>(enemy.y1);
float b2_x = static_cast<float>(enemy.x2);
float b2_y = static_cast<float>(enemy.y2);
// Si estamos arrastrando una boundary de este enemigo, usar la posición snapped
if (drag_.index == i) {
if (drag_.target == DragTarget::ENEMY_BOUND1) {
b1_x = drag_.snap_x;
b1_y = drag_.snap_y;
} else if (drag_.target == DragTarget::ENEMY_BOUND2) {
b2_x = drag_.snap_x;
b2_y = drag_.snap_y;
} else if (drag_.target == DragTarget::ENEMY_INITIAL) {
init_x = drag_.snap_x;
init_y = drag_.snap_y;
}
}
// Dibujar líneas de ruta
game_surface->drawLine(b1_x + HALF, b1_y + HALF, init_x + HALF, init_y + HALF, COLOR_ROUTE);
game_surface->drawLine(init_x + HALF, init_y + HALF, b2_x + HALF, b2_y + HALF, COLOR_ROUTE);
// Marcadores en las boundaries
renderBoundaryMarker(b1_x, b1_y, COLOR_BOUND1);
renderBoundaryMarker(b2_x, b2_y, COLOR_BOUND2);
}
}
// Dibuja un marcador de boundary (rectángulo pequeño) en una posición
void MapEditor::renderBoundaryMarker(float x, float y, Uint8 color) {
auto game_surface = Screen::get()->getRendererSurface();
if (!game_surface) { return; }
SDL_FRect marker = {.x = x, .y = y, .w = static_cast<float>(Tile::SIZE), .h = static_cast<float>(Tile::SIZE)};
game_surface->drawRectBorder(&marker, color);
}
// Convierte coordenadas de ventana a coordenadas de juego y tile
void MapEditor::updateMousePosition() {
float mouse_x = 0.0F;
float mouse_y = 0.0F;
SDL_GetMouseState(&mouse_x, &mouse_y);
float render_x = 0.0F;
float render_y = 0.0F;
SDL_RenderCoordinatesFromWindow(Screen::get()->getRenderer(), mouse_x, mouse_y, &render_x, &render_y);
SDL_FRect dst_rect = Screen::get()->getGameSurfaceDstRect();
mouse_game_x_ = render_x - dst_rect.x;
mouse_game_y_ = render_y - dst_rect.y;
// Convertir a coordenadas de tile (clampeadas al área de juego)
mouse_tile_x_ = static_cast<int>(mouse_game_x_) / Tile::SIZE;
mouse_tile_y_ = static_cast<int>(mouse_game_y_) / Tile::SIZE;
// Clampear a los límites del mapa
if (mouse_tile_x_ < 0) { mouse_tile_x_ = 0; }
if (mouse_tile_x_ >= PlayArea::WIDTH / Tile::SIZE) { mouse_tile_x_ = PlayArea::WIDTH / Tile::SIZE - 1; }
if (mouse_tile_y_ < 0) { mouse_tile_y_ = 0; }
if (mouse_tile_y_ >= PlayArea::HEIGHT / Tile::SIZE) { mouse_tile_y_ = PlayArea::HEIGHT / Tile::SIZE - 1; }
}
// Actualiza la información de la barra de estado
void MapEditor::updateStatusBarInfo() {
if (!statusbar_) { return; }
statusbar_->setMouseTile(mouse_tile_x_, mouse_tile_y_);
std::string sel_info;
std::string sel_detail;
// Info de drag activo (prioridad)
if (drag_.target != DragTarget::NONE && drag_.moved) {
switch (drag_.target) {
case DragTarget::PLAYER: sel_info = "player"; break;
case DragTarget::ENEMY_INITIAL: sel_info = "enemy " + std::to_string(drag_.index); break;
case DragTarget::ENEMY_BOUND1: sel_info = "e" + std::to_string(drag_.index) + " bound1"; break;
case DragTarget::ENEMY_BOUND2: sel_info = "e" + std::to_string(drag_.index) + " bound2"; break;
case DragTarget::ITEM: sel_info = "item " + std::to_string(drag_.index); break;
case DragTarget::NONE: break;
}
}
// Info de enemigo seleccionado (persistente)
else if (selected_enemy_ >= 0 && selected_enemy_ < static_cast<int>(room_data_.enemies.size())) {
const auto& e = room_data_.enemies[selected_enemy_];
// Extraer nombre corto de la animación (sin .yaml)
std::string anim = e.animation_path;
auto dot = anim.rfind('.');
if (dot != std::string::npos) { anim = anim.substr(0, dot); }
sel_info = "enemy " + std::to_string(selected_enemy_) + ": " + anim;
sel_detail = e.color + " vx:" + std::to_string(static_cast<int>(e.vx)) +
" vy:" + std::to_string(static_cast<int>(e.vy));
if (e.flip) { sel_detail += " flip"; }
if (e.mirror) { sel_detail += " mirror"; }
}
statusbar_->setSelectionInfo(sel_info);
statusbar_->setSelectionDetail(sel_detail);
// Actualizar el prompt de la consola según la selección
if (selected_enemy_ >= 0) {
Console::get()->setPrompt("enemy " + std::to_string(selected_enemy_) + "> ");
} else {
Console::get()->setPrompt("> ");
}
}
// ¿Hay un enemigo seleccionado?
auto MapEditor::hasSelectedEnemy() const -> bool {
return selected_enemy_ >= 0 && selected_enemy_ < static_cast<int>(room_data_.enemies.size());
}
// Modifica una propiedad del enemigo seleccionado
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"; }
auto& enemy = room_data_.enemies[selected_enemy_];
if (property == "ANIMATION") {
std::string anim = toLower(value);
if (anim.find('.') == std::string::npos) { anim += ".yaml"; }
// Intentar recrear el enemigo con la nueva animación
std::string old_anim = enemy.animation_path;
enemy.animation_path = anim;
try {
auto* enemy_mgr = room_->getEnemyManager();
enemy_mgr->getEnemy(selected_enemy_) = std::make_shared<Enemy>(enemy);
} catch (const std::exception& e) {
enemy.animation_path = old_anim; // Restaurar si falla
return std::string("Error: ") + e.what();
}
autosave();
return "animation: " + anim;
}
if (property == "COLOR") {
std::string color = toLower(value);
// Intentar recrear el enemigo con el nuevo color
std::string old_color = enemy.color;
enemy.color = color;
try {
auto* enemy_mgr = room_->getEnemyManager();
enemy_mgr->getEnemy(selected_enemy_) = std::make_shared<Enemy>(enemy);
} catch (const std::exception& e) {
enemy.color = old_color;
return std::string("Error: ") + e.what();
}
autosave();
return "color: " + color;
}
if (property == "VX") {
try {
enemy.vx = std::stof(value);
} catch (...) { return "Invalid value: " + value; }
enemy.vy = 0.0F; // No se permiten velocidades en los dos ejes
room_->getEnemyManager()->getEnemy(selected_enemy_)->resetToInitialPosition(enemy);
autosave();
return "vx: " + std::to_string(static_cast<int>(enemy.vx)) + " vy: 0";
}
if (property == "VY") {
try {
enemy.vy = std::stof(value);
} catch (...) { return "Invalid value: " + value; }
enemy.vx = 0.0F; // No se permiten velocidades en los dos ejes
room_->getEnemyManager()->getEnemy(selected_enemy_)->resetToInitialPosition(enemy);
autosave();
return "vy: " + std::to_string(static_cast<int>(enemy.vy)) + " vx: 0";
}
if (property == "FLIP") {
enemy.flip = (value == "ON" || value == "TRUE" || value == "1");
if (enemy.flip) { enemy.mirror = false; } // Mutuamente excluyentes
// Recrear el enemigo (flip/mirror se aplican en el constructor)
try {
auto* enemy_mgr = room_->getEnemyManager();
enemy_mgr->getEnemy(selected_enemy_) = std::make_shared<Enemy>(enemy);
} catch (const std::exception& e) {
return std::string("Error: ") + e.what();
}
autosave();
return std::string("flip: ") + (enemy.flip ? "on" : "off");
}
if (property == "MIRROR") {
enemy.mirror = (value == "ON" || value == "TRUE" || value == "1");
if (enemy.mirror) { enemy.flip = false; } // Mutuamente excluyentes
// Recrear el enemigo (flip/mirror se aplican en el constructor)
try {
auto* enemy_mgr = room_->getEnemyManager();
enemy_mgr->getEnemy(selected_enemy_) = std::make_shared<Enemy>(enemy);
} catch (const std::exception& e) {
return std::string("Error: ") + e.what();
}
autosave();
return std::string("mirror: ") + (enemy.mirror ? "on" : "off");
}
return "Unknown property: " + property + " (use: animation, color, vx, vy, flip, mirror)";
}
// Crea un nuevo enemigo con valores por defecto, centrado en la habitación
auto MapEditor::addEnemy() -> std::string {
if (!active_) { return "Editor not active"; }
constexpr float CENTER_X = PlayArea::CENTER_X;
constexpr float CENTER_Y = PlayArea::CENTER_Y;
constexpr float ROUTE_HALF = 2.0F * Tile::SIZE; // 2 tiles a cada lado (5 tiles total con el centro)
Enemy::Data new_enemy;
new_enemy.animation_path = "spider.yaml";
new_enemy.x = CENTER_X;
new_enemy.y = CENTER_Y;
new_enemy.vx = 24.0F;
new_enemy.vy = 0.0F;
new_enemy.x1 = static_cast<int>(CENTER_X - ROUTE_HALF);
new_enemy.y1 = static_cast<int>(CENTER_Y);
new_enemy.x2 = static_cast<int>(CENTER_X + ROUTE_HALF);
new_enemy.y2 = static_cast<int>(CENTER_Y);
new_enemy.color = "white";
new_enemy.flip = true;
new_enemy.frame = -1;
// Añadir a los datos y crear el sprite vivo
room_data_.enemies.push_back(new_enemy);
room_->getEnemyManager()->addEnemy(std::make_shared<Enemy>(new_enemy));
// Seleccionar el nuevo enemigo
selected_enemy_ = static_cast<int>(room_data_.enemies.size()) - 1;
autosave();
return "Added enemy " + std::to_string(selected_enemy_);
}
// Elimina el enemigo seleccionado
auto MapEditor::deleteEnemy() -> std::string {
if (!active_) { return "Editor not active"; }
if (!hasSelectedEnemy()) { return "No enemy selected"; }
const int IDX = selected_enemy_;
// Eliminar de los datos
room_data_.enemies.erase(room_data_.enemies.begin() + IDX);
// Recrear todos los enemigos vivos (los índices cambian al borrar)
auto* enemy_mgr = room_->getEnemyManager();
enemy_mgr->clear();
for (const auto& enemy_data : room_data_.enemies) {
enemy_mgr->addEnemy(std::make_shared<Enemy>(enemy_data));
}
selected_enemy_ = -1;
autosave();
return "Deleted enemy " + std::to_string(IDX);
}
// Duplica el enemigo seleccionado (lo pone un tile a la derecha)
auto MapEditor::duplicateEnemy() -> std::string {
if (!active_) { return "Editor not active"; }
if (!hasSelectedEnemy()) { return "No enemy selected"; }
// Copiar datos del enemigo seleccionado
Enemy::Data copy = room_data_.enemies[selected_enemy_];
// Desplazar un tile a la derecha
copy.x += Tile::SIZE;
copy.x1 += Tile::SIZE;
copy.x2 += Tile::SIZE;
// Añadir y crear sprite vivo
room_data_.enemies.push_back(copy);
room_->getEnemyManager()->addEnemy(std::make_shared<Enemy>(copy));
// Seleccionar el nuevo enemigo
selected_enemy_ = static_cast<int>(room_data_.enemies.size()) - 1;
autosave();
return "Duplicated as enemy " + std::to_string(selected_enemy_);
}
#endif // _DEBUG

View File

@@ -0,0 +1,106 @@
#pragma once
#ifdef _DEBUG
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr, unique_ptr
#include <string> // Para string
#include "external/fkyaml_node.hpp" // Para fkyaml::node
#include "game/entities/enemy.hpp" // Para Enemy::Data
#include "game/entities/item.hpp" // Para Item::Data
#include "game/entities/player.hpp" // Para Player::SpawnData
#include "game/gameplay/room.hpp" // Para Room::Data
#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data
#include "game/options.hpp" // Para Options::Cheat
class EditorStatusBar;
class MapEditor {
public:
static void init(); // [SINGLETON] Crea el objeto
static void destroy(); // [SINGLETON] Destruye el objeto
static auto get() -> MapEditor*; // [SINGLETON] Obtiene el objeto
void enter(std::shared_ptr<Room> room, std::shared_ptr<Player> player, const std::string& room_path, std::shared_ptr<Scoreboard::Data> scoreboard_data);
void exit();
[[nodiscard]] auto isActive() const -> bool { return active_; }
void update(float delta_time);
void render();
void handleEvent(const SDL_Event& event);
auto revert() -> std::string;
// Comandos para enemigos (llamados desde console_commands)
auto setEnemyProperty(const std::string& property, const std::string& value) -> std::string;
auto addEnemy() -> std::string;
auto deleteEnemy() -> std::string;
auto duplicateEnemy() -> std::string;
[[nodiscard]] auto hasSelectedEnemy() const -> bool;
private:
static MapEditor* instance_; // [SINGLETON] Objeto privado
MapEditor(); // Constructor
~MapEditor(); // Destructor
// Tipos para drag & drop y selección
enum class DragTarget { NONE, PLAYER, ENEMY_INITIAL, ENEMY_BOUND1, ENEMY_BOUND2, ITEM };
struct DragState {
DragTarget target{DragTarget::NONE};
int index{-1};
float offset_x{0.0F};
float offset_y{0.0F};
float snap_x{0.0F};
float snap_y{0.0F};
bool moved{false}; // true si el ratón se movió durante el drag
};
// Métodos internos
void updateMousePosition();
void renderEnemyBoundaries();
void renderBoundaryMarker(float x, float y, Uint8 color);
void renderSelectionHighlight();
void handleMouseDown(float game_x, float game_y);
void handleMouseUp();
void updateDrag();
void autosave();
void updateStatusBarInfo();
static auto snapToGrid(float value) -> float;
static auto pointInRect(float px, float py, const SDL_FRect& rect) -> bool;
// Estado del editor
bool active_{false};
DragState drag_;
int selected_enemy_{-1}; // Índice del enemigo seleccionado (-1 = ninguno)
// Datos de la habitación
Room::Data room_data_;
std::string room_path_;
std::string file_path_;
// YAML: nodo original (para campos que no se editan: name_ca, etc.)
fkyaml::node yaml_;
fkyaml::node yaml_backup_;
// Referencias a objetos vivos
std::shared_ptr<Room> room_;
std::shared_ptr<Player> player_;
std::shared_ptr<Scoreboard::Data> scoreboard_data_;
// Barra de estado del editor
std::unique_ptr<EditorStatusBar> statusbar_;
// Estado del ratón
float mouse_game_x_{0.0F};
float mouse_game_y_{0.0F};
int mouse_tile_x_{0};
int mouse_tile_y_{0};
// Estado previo de invencibilidad
Options::Cheat::State invincible_before_editor_{Options::Cheat::State::DISABLED};
};
#endif // _DEBUG

View File

@@ -0,0 +1,178 @@
#ifdef _DEBUG
#include "game/editor/room_saver.hpp"
#include <cmath> // Para std::round
#include <fstream> // Para ifstream, ofstream, istreambuf_iterator
#include <iostream> // Para cout, cerr
#include <sstream> // Para ostringstream
#include "utils/defines.hpp" // Para Tile::SIZE
// Carga el YAML original directamente del filesystem (no del resource pack)
auto RoomSaver::loadYAML(const std::string& file_path) -> fkyaml::node {
std::ifstream file(file_path);
if (!file.is_open()) {
std::cerr << "RoomSaver: Cannot open " << file_path << "\n";
return {};
}
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
return fkyaml::node::deserialize(content);
}
// Convierte una room connection al formato YAML
auto RoomSaver::roomConnectionToYAML(const std::string& connection) -> std::string {
if (connection == "0" || connection.empty()) { return "null"; }
return connection;
}
// Convierte la dirección del conveyor belt a string
auto RoomSaver::conveyorBeltToString(int direction) -> std::string {
if (direction < 0) { return "left"; }
if (direction > 0) { return "right"; }
return "none";
}
// Genera el YAML completo como texto con formato compacto
auto RoomSaver::buildYAML(const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string {
std::ostringstream out;
// --- Cabecera: nombre como comentario ---
out << "# " << room_data.name << "\n";
// --- Sección room ---
out << "room:\n";
// Escribir todos los campos name_* del YAML original (preserva name_ca, name_en, etc.)
if (original_yaml.contains("room")) {
const auto& room_node = original_yaml["room"];
for (auto it = room_node.begin(); it != room_node.end(); ++it) {
const std::string key = it.key().get_value<std::string>();
if (key.substr(0, 5) == "name_") {
out << " " << key << ": \"" << it.value().get_value<std::string>() << "\"\n";
}
}
}
out << " bgColor: " << room_data.bg_color << "\n";
out << " border: " << room_data.border_color << "\n";
out << " tileSetFile: " << room_data.tile_set_file << "\n";
// Conexiones
out << "\n";
out << " # Conexiones de la habitación (null = sin conexión)\n";
out << " connections:\n";
out << " up: " << roomConnectionToYAML(room_data.upper_room) << "\n";
out << " down: " << roomConnectionToYAML(room_data.lower_room) << "\n";
out << " left: " << roomConnectionToYAML(room_data.left_room) << "\n";
out << " right: " << roomConnectionToYAML(room_data.right_room) << "\n";
// Colores de items
out << "\n";
out << " # Colores de los objetos\n";
out << " itemColor1: " << (room_data.item_color1.empty() ? "yellow" : room_data.item_color1) << "\n";
out << " itemColor2: " << (room_data.item_color2.empty() ? "magenta" : room_data.item_color2) << "\n";
// Conveyor belt
out << "\n";
out << " # Dirección de la cinta transportadora: left, none, right\n";
out << " conveyorBelt: " << conveyorBeltToString(room_data.conveyor_belt_direction) << "\n";
// --- Tilemap (16 filas × 32 columnas, formato flow) ---
out << "\n";
out << "# Tilemap: 16 filas × 32 columnas (256×192 píxeles @ 8px/tile)\n";
out << "# Índices de tiles (-1 = vacío)\n";
out << "tilemap:\n";
constexpr int MAP_WIDTH = 32;
constexpr int MAP_HEIGHT = 16;
for (int row = 0; row < MAP_HEIGHT; ++row) {
out << " - [";
for (int col = 0; col < MAP_WIDTH; ++col) {
int index = row * MAP_WIDTH + col;
if (index < static_cast<int>(room_data.tile_map.size())) {
out << room_data.tile_map[index];
} else {
out << -1;
}
if (col < MAP_WIDTH - 1) { out << ", "; }
}
out << "]\n";
}
// --- Enemigos ---
if (!room_data.enemies.empty()) {
out << "\n";
out << "# Enemigos en esta habitación\n";
out << "enemies:\n";
for (const auto& enemy : room_data.enemies) {
out << " - animation: " << enemy.animation_path << "\n";
int pos_x = static_cast<int>(std::round(enemy.x / Tile::SIZE));
int pos_y = static_cast<int>(std::round(enemy.y / Tile::SIZE));
out << " position: {x: " << pos_x << ", y: " << pos_y << "}\n";
out << " velocity: {x: " << enemy.vx << ", y: " << enemy.vy << "}\n";
int b1_x = enemy.x1 / Tile::SIZE;
int b1_y = enemy.y1 / Tile::SIZE;
int b2_x = enemy.x2 / Tile::SIZE;
int b2_y = enemy.y2 / Tile::SIZE;
out << " boundaries:\n";
out << " position1: {x: " << b1_x << ", y: " << b1_y << "}\n";
out << " position2: {x: " << b2_x << ", y: " << b2_y << "}\n";
if (!enemy.color.empty() && enemy.color != "white") {
out << " color: " << enemy.color << "\n";
}
if (enemy.flip) { out << " flip: true\n"; }
if (enemy.mirror) { out << " mirror: true\n"; }
if (enemy.frame != -1) { out << " frame: " << enemy.frame << "\n"; }
out << "\n";
}
}
// --- Items ---
if (!room_data.items.empty()) {
out << "# Objetos en esta habitación\n";
out << "items:\n";
for (const auto& item : room_data.items) {
out << " - tileSetFile: " << item.tile_set_file << "\n";
out << " tile: " << item.tile << "\n";
int item_x = static_cast<int>(std::round(item.x / Tile::SIZE));
int item_y = static_cast<int>(std::round(item.y / Tile::SIZE));
out << " position: {x: " << item_x << ", y: " << item_y << "}\n";
if (item.counter != 0) {
out << " counter: " << item.counter << "\n";
}
out << "\n";
}
}
return out.str();
}
// Guarda el YAML a disco
auto RoomSaver::saveYAML(const std::string& file_path, const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string {
std::string content = buildYAML(original_yaml, room_data);
std::ofstream file(file_path);
if (!file.is_open()) {
std::cerr << "RoomSaver: Cannot write to " << file_path << "\n";
return "Error: Cannot write to " + file_path;
}
file << content;
file.close();
const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1);
std::cout << "RoomSaver: Saved " << FILE_NAME << "\n";
return "Saved " + FILE_NAME;
}
#endif // _DEBUG

View File

@@ -0,0 +1,35 @@
#pragma once
#ifdef _DEBUG
#include <string> // Para string
#include "external/fkyaml_node.hpp" // Para fkyaml::node
#include "game/gameplay/room.hpp" // Para Room::Data
/**
* @brief Guardado de archivos YAML de habitaciones para el editor de mapas
*
* Lee el YAML original con fkyaml (para acceder a todos los campos: name_ca, name_en, etc.)
* Genera el YAML como texto formateado compacto (idéntico al formato original de los ficheros).
* Solo se usa en builds de debug.
*/
class RoomSaver {
public:
RoomSaver() = delete;
// Carga el YAML original desde disco como nodo fkyaml (lee del filesystem, no del pack)
static auto loadYAML(const std::string& file_path) -> fkyaml::node;
// Genera y guarda el YAML completo a disco
// original_yaml: nodo fkyaml con los datos originales (para campos que no se editan: name_ca, etc.)
// room_data: datos editados (posiciones de enemigos, items, etc.)
static auto saveYAML(const std::string& file_path, const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string;
private:
static auto buildYAML(const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string;
static auto roomConnectionToYAML(const std::string& connection) -> std::string;
static auto conveyorBeltToString(int direction) -> std::string;
};
#endif // _DEBUG

View File

@@ -48,6 +48,27 @@ void Enemy::update(float delta_time) {
collider_ = getRect(); collider_ = getRect();
} }
#ifdef _DEBUG
// Solo actualiza la animación sin mover al enemigo
void Enemy::updateAnimation(float delta_time) {
sprite_->animate(delta_time);
}
// Resetea el enemigo a su posición inicial (para editor)
void Enemy::resetToInitialPosition(const Data& data) {
sprite_->setPosX(data.x);
sprite_->setPosY(data.y);
sprite_->setVelX(data.vx);
sprite_->setVelY(data.vy);
const int FLIP = (should_flip_ && data.vx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
const int MIRROR = should_mirror_ ? SDL_FLIP_VERTICAL : SDL_FLIP_NONE;
sprite_->setFlip(static_cast<SDL_FlipMode>(FLIP | MIRROR));
collider_ = getRect();
}
#endif
// Comprueba si ha llegado al limite del recorrido para darse media vuelta // Comprueba si ha llegado al limite del recorrido para darse media vuelta
void Enemy::checkPath() { // NOLINT(readability-make-member-function-const) void Enemy::checkPath() { // NOLINT(readability-make-member-function-const)
if (sprite_->getPosX() > x2_ || sprite_->getPosX() < x1_) { if (sprite_->getPosX() > x2_ || sprite_->getPosX() < x1_) {

View File

@@ -29,6 +29,10 @@ class Enemy {
void render(); // Pinta el enemigo en pantalla void render(); // Pinta el enemigo en pantalla
void update(float delta_time); // Actualiza las variables del objeto void update(float delta_time); // Actualiza las variables del objeto
#ifdef _DEBUG
void updateAnimation(float delta_time); // Solo actualiza la animación sin mover al enemigo
void resetToInitialPosition(const Data& data); // Resetea el enemigo a su posición inicial (para editor)
#endif
auto getRect() -> SDL_FRect; // Devuelve el rectangulo que contiene al enemigo auto getRect() -> SDL_FRect; // Devuelve el rectangulo que contiene al enemigo
auto getCollider() -> SDL_FRect&; // Obtiene el rectangulo de colision del enemigo auto getCollider() -> SDL_FRect&; // Obtiene el rectangulo de colision del enemigo

View File

@@ -41,6 +41,14 @@ auto Item::getPos() -> SDL_FPoint { // NOLINT(readability-convert-member-functi
return P; return P;
} }
#ifdef _DEBUG
// Establece la posición del item (para editor)
void Item::setPosition(float x, float y) {
sprite_->setPosition(x, y);
collider_ = sprite_->getRect();
}
#endif
// Asigna los colores del objeto // Asigna los colores del objeto
void Item::setColors(Uint8 col1, Uint8 col2) { void Item::setColors(Uint8 col1, Uint8 col2) {
// Reinicializa el vector de colores // Reinicializa el vector de colores

View File

@@ -29,6 +29,9 @@ class Item {
auto getCollider() -> SDL_FRect& { return collider_; } // Obtiene el rectangulo de colision del objeto auto getCollider() -> SDL_FRect& { return collider_; } // Obtiene el rectangulo de colision del objeto
auto getPos() -> SDL_FPoint; // Obtiene su ubicación auto getPos() -> SDL_FPoint; // Obtiene su ubicación
void setColors(Uint8 col1, Uint8 col2); // Asigna los colores del objeto void setColors(Uint8 col1, Uint8 col2); // Asigna los colores del objeto
#ifdef _DEBUG
void setPosition(float x, float y); // Establece la posición del item (para editor)
#endif
private: private:
static constexpr float ITEM_SIZE = 8.0F; // Tamaño del item en pixels static constexpr float ITEM_SIZE = 8.0F; // Tamaño del item en pixels

View File

@@ -620,20 +620,26 @@ auto Player::handleKillingTiles() -> bool {
return false; // No se encontró ninguna colisión return false; // No se encontró ninguna colisión
} }
// Establece el color del jugador (0 = automático según cheats) // Establece el color del jugador (0 = automático según options)
void Player::setColor(Uint8 color) { void Player::setColor(Uint8 color) {
if (color != 0) { if (color != 0) {
color_ = color; color_ = color;
return; return;
} }
if (Options::cheats.invincible == Options::Cheat::State::ENABLED) { // Color personalizado desde opciones
color_ = static_cast<Uint8>(PaletteColor::CYAN); if (Options::game.player_color >= 0) {
} else if (Options::cheats.infinite_lives == Options::Cheat::State::ENABLED) { color_ = static_cast<Uint8>(Options::game.player_color);
color_ = static_cast<Uint8>(PaletteColor::YELLOW);
} else { } else {
color_ = static_cast<Uint8>(PaletteColor::WHITE); color_ = static_cast<Uint8>(PaletteColor::WHITE);
} }
// Si el color coincide con el fondo de la habitación, usar fallback
if (room_ != nullptr && color_ == room_->getBGColor()) {
color_ = (room_->getBGColor() != static_cast<Uint8>(PaletteColor::WHITE))
? static_cast<Uint8>(PaletteColor::WHITE)
: static_cast<Uint8>(PaletteColor::BRIGHT_BLACK);
}
} }
// Actualiza los puntos de colisión // Actualiza los puntos de colisión
@@ -765,11 +771,18 @@ void Player::applySpawnValues(const SpawnData& spawn) {
sprite_->setFlip(spawn.flip); sprite_->setFlip(spawn.flip);
} }
// Resuelve nombre de skin a fichero de animación
auto Player::skinToAnimationPath(const std::string& skin_name) -> std::string {
if (skin_name == "default") {
return "player.yaml";
}
return skin_name + ".yaml";
}
// Cambia la skin del jugador en caliente preservando la orientación actual // Cambia la skin del jugador en caliente preservando la orientación actual
void Player::setSkin(int skin_num) { void Player::setSkin(const std::string& skin_name) {
const auto FLIP = sprite_->getFlip(); const auto FLIP = sprite_->getFlip();
const std::string PATH = (skin_num == 2) ? "player2.yaml" : "player.yaml"; initSprite(skinToAnimationPath(skin_name));
initSprite(PATH);
sprite_->setFlip(FLIP); sprite_->setFlip(FLIP);
} }
@@ -779,7 +792,7 @@ void Player::initSprite(const std::string& animations_path) { // NOLINT(readabi
sprite_ = std::make_unique<AnimatedSprite>(animation_data); sprite_ = std::make_unique<AnimatedSprite>(animation_data);
sprite_->setWidth(WIDTH); sprite_->setWidth(WIDTH);
sprite_->setHeight(HEIGHT); sprite_->setHeight(HEIGHT);
sprite_->setCurrentAnimation("walk"); sprite_->setCurrentAnimation("default");
} }
// Actualiza la posición del sprite y las colisiones // Actualiza la posición del sprite y las colisiones

View File

@@ -100,7 +100,8 @@ class Player {
auto getCollider() -> SDL_FRect& { return collider_box_; } // Obtiene el rectangulo de colision del jugador auto getCollider() -> SDL_FRect& { return collider_box_; } // Obtiene el rectangulo de colision del jugador
auto getSpawnParams() -> SpawnData { return {.x = x_, .y = y_, .vx = vx_, .vy = vy_, .last_grounded_position = last_grounded_position_, .state = state_, .flip = sprite_->getFlip()}; } // Obtiene el estado de reaparición del jugador auto getSpawnParams() -> SpawnData { return {.x = x_, .y = y_, .vx = vx_, .vy = vy_, .last_grounded_position = last_grounded_position_, .state = state_, .flip = sprite_->getFlip()}; } // Obtiene el estado de reaparición del jugador
void setColor(Uint8 color = 0); // Establece el color del jugador (0 = automático según cheats) void setColor(Uint8 color = 0); // Establece el color del jugador (0 = automático según cheats)
void setSkin(int skin_num); // Cambia la skin del jugador en caliente (1=normal, 2=alternativa) void setSkin(const std::string& skin_name); // Cambia la skin del jugador en caliente ("default" o nombre de enemigo)
static auto skinToAnimationPath(const std::string& skin_name) -> std::string; // Resuelve nombre de skin a fichero de animación
void setRoom(std::shared_ptr<Room> room) { room_ = std::move(room); } // Establece la habitación en la que se encuentra el jugador void setRoom(std::shared_ptr<Room> room) { room_ = std::move(room); } // Establece la habitación en la que se encuentra el jugador
//[[nodiscard]] auto isAlive() const -> bool { return is_alive_ || (Options::cheats.invincible == Options::Cheat::State::ENABLED); } // Comprueba si el jugador esta vivo //[[nodiscard]] auto isAlive() const -> bool { return is_alive_ || (Options::cheats.invincible == Options::Cheat::State::ENABLED); } // Comprueba si el jugador esta vivo
[[nodiscard]] auto isAlive() const -> bool { return is_alive_; } // Comprueba si el jugador esta vivo [[nodiscard]] auto isAlive() const -> bool { return is_alive_; } // Comprueba si el jugador esta vivo

View File

@@ -3,10 +3,10 @@
#include <string> #include <string>
namespace GameControl { namespace GameControl {
// Disponible en todos los builds — refresca el color del jugador según cheats // Disponible en todos los builds — cambia la skin del jugador ("default" o nombre de enemigo)
inline std::function<void()> refresh_player_color; inline std::function<void(const std::string&)> change_player_skin;
// Disponible en todos los builds — cambia la skin del jugador (1=normal, 2=alternativa) // Disponible en todos los builds — cambia el color del jugador (-1 = automático, 0-15 = color fijo)
inline std::function<void(int)> change_player_skin; inline std::function<void(int)> change_player_color;
} // namespace GameControl } // namespace GameControl
#ifdef _DEBUG #ifdef _DEBUG
@@ -23,5 +23,11 @@ namespace GameControl {
inline std::function<std::string()> set_initial_room; inline std::function<std::string()> set_initial_room;
// Registrada por Game::Game() — guarda la posición/flip actuales del jugador como posición de inicio en debug.yaml // Registrada por Game::Game() — guarda la posición/flip actuales del jugador como posición de inicio en debug.yaml
inline std::function<std::string()> set_initial_pos; inline std::function<std::string()> set_initial_pos;
// Registradas por Game::Game() — control del editor de mapas
inline std::function<void()> enter_editor;
inline std::function<void()> exit_editor;
inline std::function<std::string()> revert_editor;
inline std::function<void()> reload_current_room; // Recarga la habitación actual desde disco
inline std::function<std::string(const std::string&)> get_adjacent_room; // Obtiene la room adyacente (UP/DOWN/LEFT/RIGHT)
} // namespace GameControl } // namespace GameControl
#endif #endif

View File

@@ -41,6 +41,33 @@ void EnemyManager::render() {
} }
} }
#ifdef _DEBUG
// Solo actualiza animaciones sin mover enemigos
void EnemyManager::updateAnimations(float delta_time) {
for (const auto& enemy : enemies_) {
enemy->updateAnimation(delta_time);
}
}
// Resetea todos los enemigos a su posición inicial
void EnemyManager::resetPositions(const std::vector<Enemy::Data>& enemy_data) {
const int COUNT = std::min(static_cast<int>(enemies_.size()), static_cast<int>(enemy_data.size()));
for (int i = 0; i < COUNT; ++i) {
enemies_[i]->resetToInitialPosition(enemy_data[i]);
}
}
// Número de enemigos
auto EnemyManager::getCount() const -> int {
return static_cast<int>(enemies_.size());
}
// Acceso a un enemigo por índice
auto EnemyManager::getEnemy(int index) -> std::shared_ptr<Enemy>& {
return enemies_.at(index);
}
#endif
// Comprueba si hay colisión con algún enemigo // Comprueba si hay colisión con algún enemigo
auto EnemyManager::checkCollision(SDL_FRect& rect) -> bool { auto EnemyManager::checkCollision(SDL_FRect& rect) -> bool {
return std::ranges::any_of(enemies_, [&rect](const auto& enemy) { return std::ranges::any_of(enemies_, [&rect](const auto& enemy) {

View File

@@ -5,7 +5,7 @@
#include <memory> // Para shared_ptr #include <memory> // Para shared_ptr
#include <vector> // Para vector #include <vector> // Para vector
class Enemy; #include "game/entities/enemy.hpp" // Para Enemy, Enemy::Data
/** /**
* @brief Gestor de enemigos de una habitación * @brief Gestor de enemigos de una habitación
@@ -40,6 +40,13 @@ class EnemyManager {
// Detección de colisiones // Detección de colisiones
auto checkCollision(SDL_FRect& rect) -> bool; // Comprueba si hay colisión con algún enemigo auto checkCollision(SDL_FRect& rect) -> bool; // Comprueba si hay colisión con algún enemigo
#ifdef _DEBUG
void updateAnimations(float delta_time); // Solo actualiza animaciones sin mover enemigos
void resetPositions(const std::vector<Enemy::Data>& enemy_data); // Resetea todos los enemigos a su posición inicial
[[nodiscard]] auto getCount() const -> int; // Número de enemigos
auto getEnemy(int index) -> std::shared_ptr<Enemy>&; // Acceso a un enemigo por índice
#endif
private: private:
std::vector<std::shared_ptr<Enemy>> enemies_; // Colección de enemigos std::vector<std::shared_ptr<Enemy>> enemies_; // Colección de enemigos
}; };

View File

@@ -6,9 +6,8 @@
#include <string> // Para string #include <string> // Para string
#include <vector> // Para vector #include <vector> // Para vector
#include "scoreboard.hpp" // Para Scoreboard::Data #include "game/entities/item.hpp" // Para Item, Item::Data
#include "scoreboard.hpp" // Para Scoreboard::Data
class Item;
/** /**
* @brief Gestor de items de una habitación * @brief Gestor de items de una habitación
@@ -47,6 +46,11 @@ class ItemManager {
// Estado // Estado
void setPaused(bool paused); // Pausa/despausa todos los items void setPaused(bool paused); // Pausa/despausa todos los items
#ifdef _DEBUG
[[nodiscard]] auto getCount() const -> int { return static_cast<int>(items_.size()); } // Número de items
auto getItem(int index) -> std::shared_ptr<Item>& { return items_.at(index); } // Acceso a un item por índice
#endif
// Detección de colisiones // Detección de colisiones
/** /**
* @brief Comprueba si hay colisión con algún item * @brief Comprueba si hay colisión con algún item

View File

@@ -121,6 +121,18 @@ void Room::renderItems() {
void Room::redrawMap() { void Room::redrawMap() {
tilemap_renderer_->redrawMap(collision_map_.get()); tilemap_renderer_->redrawMap(collision_map_.get());
} }
// Actualiza animaciones sin mover enemigos (para editor de mapas)
void Room::updateEditorMode(float delta_time) {
tilemap_renderer_->update(delta_time);
enemy_manager_->updateAnimations(delta_time);
item_manager_->update(delta_time);
}
// Resetea enemigos a posiciones iniciales (para editor de mapas)
void Room::resetEnemyPositions(const std::vector<Enemy::Data>& enemy_data) {
enemy_manager_->resetPositions(enemy_data);
}
#endif #endif
// Actualiza las variables y objetos de la habitación // Actualiza las variables y objetos de la habitación

View File

@@ -69,7 +69,11 @@ class Room {
void renderEnemies(); // Dibuja los enemigos en pantalla void renderEnemies(); // Dibuja los enemigos en pantalla
void renderItems(); // Dibuja los objetos en pantalla void renderItems(); // Dibuja los objetos en pantalla
#ifdef _DEBUG #ifdef _DEBUG
void redrawMap(); // Redibuja el mapa (para actualizar modo debug) void redrawMap(); // Redibuja el mapa (para actualizar modo debug)
void updateEditorMode(float delta_time); // Actualiza animaciones sin mover enemigos (para editor)
void resetEnemyPositions(const std::vector<Enemy::Data>& enemy_data); // Resetea enemigos a posiciones iniciales
auto getEnemyManager() -> EnemyManager* { return enemy_manager_.get(); } // Acceso al gestor de enemigos (para editor)
auto getItemManager() -> ItemManager* { return item_manager_.get(); } // Acceso al gestor de items (para editor)
#endif #endif
void update(float delta_time); // Actualiza las variables y objetos de la habitación void update(float delta_time); // Actualiza las variables y objetos de la habitación
auto getRoom(Border border) -> std::string; // Devuelve la cadena del fichero de la habitación contigua segun el borde auto getRoom(Border border) -> std::string; // Devuelve la cadena del fichero de la habitación contigua segun el borde

View File

@@ -316,6 +316,25 @@ void RoomLoader::parseItems(const fkyaml::node& yaml, Room::Data& room, bool ver
} }
} }
#ifdef _DEBUG
// Carga una habitación desde un string YAML (para el editor de mapas, evita el resource pack)
auto RoomLoader::loadFromString(const std::string& yaml_content, const std::string& file_name) -> Room::Data {
Room::Data room;
try {
auto yaml = fkyaml::node::deserialize(yaml_content);
parseRoomConfig(yaml, room, file_name);
parseTilemap(yaml, room, file_name, false);
parseEnemies(yaml, room, false);
parseItems(yaml, room, false);
} catch (const fkyaml::exception& e) {
std::cerr << "YAML parsing error in " << file_name << ": " << e.what() << '\n';
} catch (const std::exception& e) {
std::cerr << "Error loading room " << file_name << ": " << e.what() << '\n';
}
return room;
}
#endif
// Carga un archivo de room en formato YAML // Carga un archivo de room en formato YAML
auto RoomLoader::loadYAML(const std::string& file_path, bool verbose) -> Room::Data { // NOLINT(readability-convert-member-functions-to-static) auto RoomLoader::loadYAML(const std::string& file_path, bool verbose) -> Room::Data { // NOLINT(readability-convert-member-functions-to-static)
Room::Data room; Room::Data room;

View File

@@ -46,6 +46,9 @@ class RoomLoader {
* - items: lista de items (opcional) * - items: lista de items (opcional)
*/ */
static auto loadYAML(const std::string& file_path, bool verbose = false) -> Room::Data; static auto loadYAML(const std::string& file_path, bool verbose = false) -> Room::Data;
#ifdef _DEBUG
static auto loadFromString(const std::string& yaml_content, const std::string& file_name) -> Room::Data;
#endif
private: private:
/** /**

View File

@@ -10,6 +10,7 @@
#include "core/rendering/surface.hpp" // Para Surface #include "core/rendering/surface.hpp" // Para Surface
#include "core/rendering/text.hpp" // Para Text #include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource #include "core/resources/resource_cache.hpp" // Para Resource
#include "game/entities/player.hpp" // Para Player::skinToAnimationPath
#include "game/options.hpp" // Para Options, options, Cheat, OptionsGame #include "game/options.hpp" // Para Options, options, Cheat, OptionsGame
#include "utils/defines.hpp" // Para BLOCK #include "utils/defines.hpp" // Para BLOCK
#include "utils/utils.hpp" // Para stringToColor #include "utils/utils.hpp" // Para stringToColor
@@ -22,9 +23,10 @@ Scoreboard::Scoreboard(std::shared_ptr<Data> data)
constexpr float SURFACE_HEIGHT = 6.0F * Tile::SIZE; constexpr float SURFACE_HEIGHT = 6.0F * Tile::SIZE;
// Reserva memoria para los objetos // Reserva memoria para los objetos
const auto& player_animation_data = Resource::Cache::get()->getAnimationData((Options::game.player_skin == 2) ? "player2.yaml" : "player.yaml"); const std::string player_anim_path = Player::skinToAnimationPath(Options::game.player_skin);
const auto& player_animation_data = Resource::Cache::get()->getAnimationData(player_anim_path);
player_sprite_ = std::make_shared<AnimatedSprite>(player_animation_data); player_sprite_ = std::make_shared<AnimatedSprite>(player_animation_data);
player_sprite_->setCurrentAnimation("walk_menu"); player_sprite_->setCurrentAnimation("default");
surface_ = std::make_shared<Surface>(SURFACE_WIDTH, SURFACE_HEIGHT); surface_ = std::make_shared<Surface>(SURFACE_WIDTH, SURFACE_HEIGHT);
surface_dest_ = {.x = 0, .y = Options::game.height - SURFACE_HEIGHT, .w = SURFACE_WIDTH, .h = SURFACE_HEIGHT}; surface_dest_ = {.x = 0, .y = Options::game.height - SURFACE_HEIGHT, .w = SURFACE_WIDTH, .h = SURFACE_HEIGHT};
@@ -49,9 +51,6 @@ void Scoreboard::update(float delta_time) {
// Acumular tiempo para animaciones // Acumular tiempo para animaciones
time_accumulator_ += delta_time; time_accumulator_ += delta_time;
// Actualizar sprite con delta time
player_sprite_->update(delta_time);
// Actualiza el color de la cantidad de items recogidos // Actualiza el color de la cantidad de items recogidos
updateItemsColor(delta_time); updateItemsColor(delta_time);
@@ -77,6 +76,14 @@ auto Scoreboard::getTime() -> Scoreboard::ClockData { // NOLINT(readability-con
return time; return time;
} }
// Actualiza el sprite del jugador con la skin actual
void Scoreboard::refreshPlayerSkin() {
const std::string player_anim_path = Player::skinToAnimationPath(Options::game.player_skin);
const auto& player_animation_data = Resource::Cache::get()->getAnimationData(player_anim_path);
player_sprite_ = std::make_shared<AnimatedSprite>(player_animation_data);
player_sprite_->setCurrentAnimation("default");
}
// Pone el marcador en modo pausa // Pone el marcador en modo pausa
void Scoreboard::setPaused(bool value) { void Scoreboard::setPaused(bool value) {
if (is_paused_ == value) { if (is_paused_ == value) {
@@ -130,18 +137,14 @@ void Scoreboard::fillTexture() {
// Limpia la textura // Limpia la textura
surface_->clear(stringToColor("black")); surface_->clear(stringToColor("black"));
// Anclas
constexpr int LINE1 = Tile::SIZE;
constexpr int LINE2 = 3 * Tile::SIZE;
// Dibuja las vidas // Dibuja las vidas
// Calcular desplazamiento basado en tiempo const int WALK_FRAMES = player_sprite_->getCurrentAnimationSize();
const int DESP = static_cast<int>(time_accumulator_ / SPRITE_WALK_CYCLE_DURATION) % 8; const int DESP = static_cast<int>(time_accumulator_ / SPRITE_WALK_CYCLE_DURATION) % (WALK_FRAMES * 2);
const int FRAME = DESP % SPRITE_WALK_FRAMES; const int FRAME = DESP % WALK_FRAMES;
player_sprite_->setCurrentAnimationFrame(FRAME); player_sprite_->setCurrentAnimationFrame(FRAME);
player_sprite_->setPosY(LINE2); player_sprite_->setPosY(LINE2_Y);
for (int i = 0; i < data_->lives; ++i) { for (int i = 0; i < data_->lives; ++i) {
player_sprite_->setPosX(8 + (16 * i) + DESP); player_sprite_->setPosX(LIVES_START_X + (LIVES_SPACING * i) + DESP);
const int INDEX = i % color_.size(); const int INDEX = i % color_.size();
player_sprite_->render(1, color_.at(INDEX)); player_sprite_->render(1, color_.at(INDEX));
} }
@@ -150,21 +153,30 @@ void Scoreboard::fillTexture() {
if (data_->music) { if (data_->music) {
const Uint8 C = data_->color; const Uint8 C = data_->color;
SDL_FRect clip = {.x = 0, .y = 8, .w = 8, .h = 8}; SDL_FRect clip = {.x = 0, .y = 8, .w = 8, .h = 8};
item_surface_->renderWithColorReplace(20 * Tile::SIZE, LINE2, 1, C, &clip); item_surface_->renderWithColorReplace(MUSIC_ICON_X, LINE2_Y, 1, C, &clip);
} }
// Escribe los textos // Escribe los textos
auto text = Resource::Cache::get()->getText("smb2"); auto text = Resource::Cache::get()->getText("smb2");
const std::string TIME_TEXT = std::to_string((clock_.minutes % 100) / 10) + std::to_string(clock_.minutes % 10) + clock_.separator + std::to_string((clock_.seconds % 60) / 10) + std::to_string(clock_.seconds % 10); const std::string TIME_TEXT = std::to_string((clock_.minutes % 100) / 10) + std::to_string(clock_.minutes % 10) + clock_.separator + std::to_string((clock_.seconds % 60) / 10) + std::to_string(clock_.seconds % 10);
const std::string ITEMS_TEXT = std::to_string(data_->items / 100) + std::to_string((data_->items % 100) / 10) + std::to_string(data_->items % 10); const std::string ITEMS_TEXT = std::to_string(data_->items / 100) + std::to_string((data_->items % 100) / 10) + std::to_string(data_->items % 10);
text->writeColored(Tile::SIZE, LINE1, Locale::get()->get("scoreboard.items"), data_->color); // NOLINT(readability-static-accessed-through-instance) text->writeColored(ITEMS_LABEL_X, LINE1_Y, Locale::get()->get("scoreboard.items"), data_->color); // NOLINT(readability-static-accessed-through-instance)
text->writeColored(17 * Tile::SIZE, LINE1, ITEMS_TEXT, items_color_); text->writeColored(ITEMS_VALUE_X, LINE1_Y, ITEMS_TEXT, items_color_);
text->writeColored(20 * Tile::SIZE, LINE1, Locale::get()->get("scoreboard.time"), data_->color); // NOLINT(readability-static-accessed-through-instance) text->writeColored(TIME_LABEL_X, LINE1_Y, Locale::get()->get("scoreboard.time"), data_->color); // NOLINT(readability-static-accessed-through-instance)
text->writeColored(26 * Tile::SIZE, LINE1, TIME_TEXT, stringToColor("white")); text->writeColored(TIME_VALUE_X, LINE1_Y, TIME_TEXT, stringToColor("white"));
const std::string ROOMS_TEXT = std::to_string(data_->rooms / 100) + std::to_string((data_->rooms % 100) / 10) + std::to_string(data_->rooms % 10); const std::string ROOMS_TEXT = std::to_string(data_->rooms / 100) + std::to_string((data_->rooms % 100) / 10) + std::to_string(data_->rooms % 10);
text->writeColored(22 * Tile::SIZE, LINE2, Locale::get()->get("scoreboard.rooms"), stringToColor("white")); // NOLINT(readability-static-accessed-through-instance) text->writeColored(ROOMS_LABEL_X, LINE2_Y, Locale::get()->get("scoreboard.rooms"), stringToColor("white")); // NOLINT(readability-static-accessed-through-instance)
text->writeColored(28 * Tile::SIZE, LINE2, ROOMS_TEXT, stringToColor("white")); text->writeColored(ROOMS_VALUE_X, LINE2_Y, ROOMS_TEXT, stringToColor("white"));
// Indicadores de trucos activos (fuente 8bithud)
auto cheat_text = Resource::Cache::get()->getText("8bithud");
if (Options::cheats.infinite_lives == Options::Cheat::State::ENABLED) {
cheat_text->writeColored(CHEAT_INF_LIVES_X, CHEAT_INF_LIVES_Y, Locale::get()->get("scoreboard.cheat_infinite_lives"), data_->color); // NOLINT(readability-static-accessed-through-instance)
}
if (Options::cheats.invincible == Options::Cheat::State::ENABLED) {
cheat_text->writeColored(CHEAT_INVINCIBLE_X, CHEAT_INVINCIBLE_Y, Locale::get()->get("scoreboard.cheat_invincibility"), data_->color); // NOLINT(readability-static-accessed-through-instance)
}
// Deja el renderizador como estaba // Deja el renderizador como estaba
Screen::get()->setRendererSurface(previuos_renderer); Screen::get()->setRendererSurface(previuos_renderer);

View File

@@ -28,6 +28,7 @@ class Scoreboard {
void render(); // Pinta el objeto en pantalla void render(); // Pinta el objeto en pantalla
void update(float delta_time); // Actualiza las variables del objeto void update(float delta_time); // Actualiza las variables del objeto
void setPaused(bool value); // Pone el marcador en modo pausa void setPaused(bool value); // Pone el marcador en modo pausa
void refreshPlayerSkin(); // Actualiza el sprite del jugador con la skin actual
auto getMinutes() -> int; // Devuelve la cantidad de minutos de juego transcurridos auto getMinutes() -> int; // Devuelve la cantidad de minutos de juego transcurridos
private: private:
@@ -42,7 +43,23 @@ class Scoreboard {
// Constantes de tiempo // Constantes de tiempo
static constexpr float ITEMS_COLOR_BLINK_DURATION = 0.333F; // Duración de cada estado del parpadeo (era 10 frames @ 60fps) static constexpr float ITEMS_COLOR_BLINK_DURATION = 0.333F; // Duración de cada estado del parpadeo (era 10 frames @ 60fps)
static constexpr float SPRITE_WALK_CYCLE_DURATION = 0.667F; // Duración del ciclo de caminar (era 40 frames @ 60fps) static constexpr float SPRITE_WALK_CYCLE_DURATION = 0.667F; // Duración del ciclo de caminar (era 40 frames @ 60fps)
static constexpr int SPRITE_WALK_FRAMES = 4; // Número de frames de animación
// Posición de los elementos del marcador (en pixels, Tile::SIZE = 8)
static constexpr int LINE1_Y = 8; // Fila superior (1 * Tile::SIZE)
static constexpr int LINE2_Y = 24; // Fila inferior (3 * Tile::SIZE)
static constexpr int ITEMS_LABEL_X = 8; // "TRESORS PILLATS" / "ITEMS COLLECTED"
static constexpr int ITEMS_VALUE_X = 136; // Valor numérico de items
static constexpr int TIME_LABEL_X = 160; // "HORA" / "TIME"
static constexpr int TIME_VALUE_X = 208; // Valor numérico del tiempo
static constexpr int LIVES_START_X = 8; // Primera vida (sprite)
static constexpr int LIVES_SPACING = 16; // Separación entre vidas
static constexpr int MUSIC_ICON_X = 160; // Icono de música
static constexpr int ROOMS_LABEL_X = 176; // "SALES" / "ROOMS"
static constexpr int ROOMS_VALUE_X = 224; // Valor numérico de salas
static constexpr int CHEAT_INF_LIVES_X = 176; // Indicador "vides inf" / "inf lives"
static constexpr int CHEAT_INF_LIVES_Y = 34; // Posición Y del indicador de vidas infinitas
static constexpr int CHEAT_INVINCIBLE_X = 231; // Indicador "inv"
static constexpr int CHEAT_INVINCIBLE_Y = 34; // Posición Y del indicador de invencibilidad
// Métodos privados // Métodos privados
auto getTime() -> ClockData; // Obtiene el tiempo transcurrido de partida auto getTime() -> ClockData; // Obtiene el tiempo transcurrido de partida

View File

@@ -11,6 +11,7 @@
#include "core/input/input_types.hpp" // Para BUTTON_TO_STRING, STRING_TO_BUTTON #include "core/input/input_types.hpp" // Para BUTTON_TO_STRING, STRING_TO_BUTTON
#include "external/fkyaml_node.hpp" // Para fkyaml::node #include "external/fkyaml_node.hpp" // Para fkyaml::node
#include "game/defaults.hpp" // Para GameDefaults::VERSION #include "game/defaults.hpp" // Para GameDefaults::VERSION
#include "utils/utils.hpp" // Para toLower
namespace Options { namespace Options {
@@ -177,24 +178,6 @@ namespace Options {
{"LEFT_STICK_LEFT", 200}, {"LEFT_STICK_LEFT", 200},
{"LEFT_STICK_RIGHT", 201}}; {"LEFT_STICK_RIGHT", 201}};
// Lista de paletas válidas
const std::vector<std::string> VALID_PALETTES = {
"black-and-white",
"green-phosphor",
"island-joy-16",
"lost-century",
"na16",
"orange-screen",
"pico-8",
"ruzx-spectrum",
"ruzx-spectrum-revision-2",
"steam-lords",
"sweetie-16",
"sweetie-16_bona",
"zx-spectrum",
"zx-spectrum-adjusted",
"zxarne-5-2"};
// Funciones helper de conversión // Funciones helper de conversión
auto filterToString(Screen::Filter filter) -> std::string { auto filterToString(Screen::Filter filter) -> std::string {
auto it = FILTER_TO_STRING.find(filter); auto it = FILTER_TO_STRING.find(filter);
@@ -257,12 +240,6 @@ namespace Options {
} }
} }
auto isValidPalette(const std::string& palette) -> bool {
std::string lower_palette = palette;
std::ranges::transform(lower_palette, lower_palette.begin(), ::tolower);
return std::ranges::any_of(VALID_PALETTES, [&lower_palette](const auto& valid) { return valid == lower_palette; });
}
// --- Funciones helper para loadFromFile() --- // --- Funciones helper para loadFromFile() ---
// Carga configuración de ventana desde YAML // Carga configuración de ventana desde YAML
@@ -309,20 +286,99 @@ namespace Options {
} }
} }
// Helper: carga el campo palette con validación extra // Helper: carga el campo palette; PaletteManager hará el fallback si el nombre no existe
void loadPaletteFromYaml(const fkyaml::node& vid) { void loadPaletteFromYaml(const fkyaml::node& vid) {
if (!vid.contains("palette")) { return; } if (vid.contains("palette")) {
try { try {
auto palette_str = vid["palette"].get_value<std::string>(); auto palette_str = vid["palette"].get_value<std::string>();
video.palette = isValidPalette(palette_str) ? palette_str : Defaults::Video::PALETTE_NAME; video.palette = toLower(palette_str);
} catch (...) { } catch (...) {
video.palette = Defaults::Video::PALETTE_NAME; video.palette = Defaults::Video::PALETTE_NAME;
}
}
if (vid.contains("palette_sort")) {
try {
video.palette_sort = toLower(vid["palette_sort"].get_value<std::string>());
} catch (...) {
video.palette_sort = Defaults::Video::PALETTE_SORT;
}
} }
} }
// Carga los campos básicos de configuración de video // Helper: carga la sección gpu desde YAML
void loadGPUConfigFromYaml(const fkyaml::node& gpu_node) {
if (gpu_node.contains("acceleration")) {
try {
video.gpu.acceleration = gpu_node["acceleration"].get_value<bool>();
} catch (...) {
video.gpu.acceleration = Defaults::Video::GPU_ACCELERATION;
}
}
if (gpu_node.contains("preferred_driver")) {
try {
video.gpu.preferred_driver = gpu_node["preferred_driver"].get_value<std::string>();
} catch (...) {
video.gpu.preferred_driver = "";
}
}
}
// Helper: carga la sección supersampling desde YAML
void loadSupersamplingConfigFromYaml(const fkyaml::node& ss_node) {
if (ss_node.contains("enabled")) {
try {
video.supersampling.enabled = ss_node["enabled"].get_value<bool>();
} catch (...) {
video.supersampling.enabled = Defaults::Video::SUPERSAMPLING;
}
}
if (ss_node.contains("linear_upscale")) {
try {
video.supersampling.linear_upscale = ss_node["linear_upscale"].get_value<bool>();
} catch (...) {
video.supersampling.linear_upscale = Defaults::Video::LINEAR_UPSCALE;
}
}
if (ss_node.contains("downscale_algo")) {
try {
video.supersampling.downscale_algo = ss_node["downscale_algo"].get_value<int>();
} catch (...) {
video.supersampling.downscale_algo = Defaults::Video::DOWNSCALE_ALGO;
}
}
}
// Helper: carga la sección shader desde YAML
void loadShaderConfigFromYaml(const fkyaml::node& sh_node) {
if (sh_node.contains("enabled")) {
try {
video.shader.enabled = sh_node["enabled"].get_value<bool>();
} catch (...) {
video.shader.enabled = Defaults::Video::SHADER_ENABLED;
}
}
if (sh_node.contains("current_shader")) {
try {
const std::string s = sh_node["current_shader"].get_value<std::string>();
video.shader.current_shader = (s == "crtpi") ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX;
} catch (...) {
video.shader.current_shader = Rendering::ShaderType::POSTFX;
}
}
if (sh_node.contains("current_postfx_preset")) {
try {
video.shader.current_postfx_preset_name = sh_node["current_postfx_preset"].get_value<std::string>();
} catch (...) {}
}
if (sh_node.contains("current_crtpi_preset")) {
try {
video.shader.current_crtpi_preset_name = sh_node["current_crtpi_preset"].get_value<std::string>();
} catch (...) {}
}
}
// Carga los campos básicos de configuración de video (nivel video:)
void loadBasicVideoFieldsFromYaml(const fkyaml::node& vid) { void loadBasicVideoFieldsFromYaml(const fkyaml::node& vid) {
// fullscreen (antes era "mode")
if (vid.contains("fullscreen")) { if (vid.contains("fullscreen")) {
try { try {
video.fullscreen = vid["fullscreen"].get_value<bool>(); video.fullscreen = vid["fullscreen"].get_value<bool>();
@@ -331,7 +387,6 @@ namespace Options {
} }
} }
// filter (ahora es string)
if (vid.contains("filter")) { if (vid.contains("filter")) {
try { try {
auto filter_str = vid["filter"].get_value<std::string>(); auto filter_str = vid["filter"].get_value<std::string>();
@@ -341,47 +396,6 @@ namespace Options {
} }
} }
if (vid.contains("postfx")) {
try {
video.postfx = vid["postfx"].get_value<bool>();
} catch (...) {
video.postfx = Defaults::Video::POSTFX;
}
}
if (vid.contains("supersampling")) {
try {
video.supersampling = vid["supersampling"].get_value<bool>();
} catch (...) {
video.supersampling = Defaults::Video::SUPERSAMPLING;
}
}
if (vid.contains("current_postfx_preset")) {
try {
current_postfx_preset = vid["current_postfx_preset"].get_value<int>();
} catch (...) {
current_postfx_preset = 0;
}
}
if (vid.contains("current_crtpi_preset")) {
try {
current_crtpi_preset = vid["current_crtpi_preset"].get_value<int>();
} catch (...) {
current_crtpi_preset = 0;
}
}
if (vid.contains("current_shader")) {
try {
const std::string s = vid["current_shader"].get_value<std::string>();
current_shader = (s == "crtpi") ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX;
} catch (...) {
current_shader = Rendering::ShaderType::POSTFX;
}
}
if (vid.contains("vertical_sync")) { if (vid.contains("vertical_sync")) {
try { try {
video.vertical_sync = vid["vertical_sync"].get_value<bool>(); video.vertical_sync = vid["vertical_sync"].get_value<bool>();
@@ -406,30 +420,6 @@ namespace Options {
} }
} }
if (vid.contains("linear_upscale")) {
try {
video.linear_upscale = vid["linear_upscale"].get_value<bool>();
} catch (...) {
video.linear_upscale = Defaults::Video::LINEAR_UPSCALE;
}
}
if (vid.contains("downscale_algo")) {
try {
video.downscale_algo = vid["downscale_algo"].get_value<int>();
} catch (...) {
video.downscale_algo = Defaults::Video::DOWNSCALE_ALGO;
}
}
if (vid.contains("gpu_preferred_driver")) {
try {
video.gpu_preferred_driver = vid["gpu_preferred_driver"].get_value<std::string>();
} catch (...) {
video.gpu_preferred_driver = "";
}
}
loadPaletteFromYaml(vid); loadPaletteFromYaml(vid);
} }
@@ -439,10 +429,18 @@ namespace Options {
const auto& vid = yaml["video"]; const auto& vid = yaml["video"];
loadBasicVideoFieldsFromYaml(vid); loadBasicVideoFieldsFromYaml(vid);
// Lee border
if (vid.contains("border")) { if (vid.contains("border")) {
loadBorderConfigFromYaml(vid["border"]); loadBorderConfigFromYaml(vid["border"]);
} }
if (vid.contains("gpu")) {
loadGPUConfigFromYaml(vid["gpu"]);
}
if (vid.contains("supersampling")) {
loadSupersamplingConfigFromYaml(vid["supersampling"]);
}
if (vid.contains("shader")) {
loadShaderConfigFromYaml(vid["shader"]);
}
} }
} }
@@ -520,12 +518,19 @@ namespace Options {
const auto& player_node = yaml["player"]; const auto& player_node = yaml["player"];
if (player_node.contains("skin")) { if (player_node.contains("skin")) {
try { try {
int skin = player_node["skin"].get_value<int>(); game.player_skin = player_node["skin"].get_value<std::string>();
game.player_skin = (skin == 2) ? 2 : Defaults::Game::Player::SKIN;
} catch (...) { } catch (...) {
game.player_skin = Defaults::Game::Player::SKIN; game.player_skin = Defaults::Game::Player::SKIN;
} }
} }
if (player_node.contains("color")) {
try {
int color = player_node["color"].get_value<int>();
game.player_color = (color >= 0 && color <= 15) ? color : Defaults::Game::Player::COLOR;
} catch (...) {
game.player_color = Defaults::Game::Player::COLOR;
}
}
} }
} }
@@ -681,6 +686,25 @@ namespace Options {
} }
} }
// Helper: retorna el nombre del preset PostFX actual (para guardar en config)
auto currentPostFXPresetName() -> std::string {
const auto idx = static_cast<size_t>(video.shader.current_postfx_preset);
if (idx < postfx_presets.size()) {
return postfx_presets[idx].name;
}
// Presets no cargados aún: devolver el nombre almacenado del config
return video.shader.current_postfx_preset_name;
}
// Helper: retorna el nombre del preset CrtPi actual (para guardar en config)
auto currentCrtPiPresetName() -> std::string {
const auto idx = static_cast<size_t>(video.shader.current_crtpi_preset);
if (idx < crtpi_presets.size()) {
return crtpi_presets[idx].name;
}
return video.shader.current_crtpi_preset_name;
}
// Guarda las opciones al fichero configurado // Guarda las opciones al fichero configurado
auto saveToFile() -> bool { auto saveToFile() -> bool {
// Abre el fichero para escritura // Abre el fichero para escritura
@@ -727,23 +751,28 @@ namespace Options {
file << "# VIDEO \n"; file << "# VIDEO \n";
file << "video:\n"; file << "video:\n";
file << " fullscreen: " << (video.fullscreen ? "true" : "false") << "\n"; file << " fullscreen: " << (video.fullscreen ? "true" : "false") << "\n";
file << " filter: " << filterToString(video.filter) << " # filter: nearest (pixel perfect) | linear (smooth)\n";
file << " postfx: " << (video.postfx ? "true" : "false") << "\n";
file << " supersampling: " << (video.supersampling ? "true" : "false") << "\n";
file << " current_postfx_preset: " << current_postfx_preset << "\n";
file << " current_crtpi_preset: " << current_crtpi_preset << "\n";
file << " current_shader: " << (current_shader == Rendering::ShaderType::CRTPI ? "crtpi" : "postfx") << "\n";
file << " vertical_sync: " << (video.vertical_sync ? "true" : "false") << "\n"; file << " vertical_sync: " << (video.vertical_sync ? "true" : "false") << "\n";
file << " integer_scale: " << (video.integer_scale ? "true" : "false") << "\n"; file << " integer_scale: " << (video.integer_scale ? "true" : "false") << "\n";
file << " keep_aspect: " << (video.keep_aspect ? "true" : "false") << "\n"; file << " keep_aspect: " << (video.keep_aspect ? "true" : "false") << "\n";
file << " linear_upscale: " << (video.linear_upscale ? "true" : "false") << "\n"; file << " filter: " << filterToString(video.filter) << " # filter: nearest (pixel perfect) | linear (smooth)\n";
file << " downscale_algo: " << video.downscale_algo << " # 0=bilinear, 1=Lanczos2, 2=Lanczos3\n";
file << " gpu_preferred_driver: \"" << video.gpu_preferred_driver << "\" # GPU driver (empty = auto)\n";
file << " palette: " << video.palette << "\n"; file << " palette: " << video.palette << "\n";
file << " palette_sort: " << video.palette_sort << "\n";
file << " border:\n"; file << " border:\n";
file << " enabled: " << (video.border.enabled ? "true" : "false") << "\n"; file << " enabled: " << (video.border.enabled ? "true" : "false") << "\n";
file << " width: " << video.border.width << "\n"; file << " width: " << video.border.width << "\n";
file << " height: " << video.border.height << "\n"; file << " height: " << video.border.height << "\n";
file << " gpu:\n";
file << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << " # Usar aceleración hardware GPU (false = SDL fallback)\n";
file << " preferred_driver: \"" << video.gpu.preferred_driver << "\" # Driver GPU específico (empty = auto, aplica solo si gpu_acceleration: true)\n";
file << " supersampling:\n";
file << " enabled: " << (video.supersampling.enabled ? "true" : "false") << "\n";
file << " linear_upscale: " << (video.supersampling.linear_upscale ? "true" : "false") << "\n";
file << " downscale_algo: " << video.supersampling.downscale_algo << " # 0=bilinear, 1=Lanczos2, 2=Lanczos3\n";
file << " shader:\n";
file << " enabled: " << (video.shader.enabled ? "true" : "false") << "\n";
file << " current_shader: " << (video.shader.current_shader == Rendering::ShaderType::CRTPI ? "crtpi" : "postfx") << "\n";
file << " current_postfx_preset: " << currentPostFXPresetName() << "\n";
file << " current_crtpi_preset: " << currentCrtPiPresetName() << "\n";
file << "\n"; file << "\n";
// KEYBOARD CONTROLS // KEYBOARD CONTROLS
@@ -765,7 +794,8 @@ namespace Options {
file << "\n"; file << "\n";
file << "# PLAYER\n"; file << "# PLAYER\n";
file << "player:\n"; file << "player:\n";
file << " skin: " << game.player_skin << "\n"; file << " skin: \"" << game.player_skin << "\"\n";
file << " color: " << game.player_color << "\n";
file << "\n"; file << "\n";
file << "# KIOSK MODE\n"; file << "# KIOSK MODE\n";
@@ -787,6 +817,40 @@ namespace Options {
return true; return true;
} }
// Resuelve el nombre del preset PostFX a índice dentro del vector de presets
void resolvePostFXPresetName() {
const auto& name = video.shader.current_postfx_preset_name;
if (name.empty()) {
video.shader.current_postfx_preset = 0;
return;
}
for (int i = 0; i < static_cast<int>(postfx_presets.size()); ++i) {
if (postfx_presets[static_cast<size_t>(i)].name == name) {
video.shader.current_postfx_preset = i;
return;
}
}
std::cout << "PostFX preset '" << name << "' not found, defaulting to first preset\n";
video.shader.current_postfx_preset = 0;
}
// Resuelve el nombre del preset CrtPi a índice dentro del vector de presets
void resolveCrtPiPresetName() {
const auto& name = video.shader.current_crtpi_preset_name;
if (name.empty()) {
video.shader.current_crtpi_preset = 0;
return;
}
for (int i = 0; i < static_cast<int>(crtpi_presets.size()); ++i) {
if (crtpi_presets[static_cast<size_t>(i)].name == name) {
video.shader.current_crtpi_preset = i;
return;
}
}
std::cout << "CrtPi preset '" << name << "' not found, defaulting to first preset\n";
video.shader.current_crtpi_preset = 0;
}
// Establece la ruta del fichero de PostFX // Establece la ruta del fichero de PostFX
void setPostFXFile(const std::string& path) { void setPostFXFile(const std::string& path) {
postfx_file_path = path; postfx_file_path = path;
@@ -838,14 +902,11 @@ namespace Options {
} }
} }
// Preservar el índice cargado desde config.yaml; clampar al rango válido. // Resolver el nombre del preset a índice
if (!postfx_presets.empty()) { if (!postfx_presets.empty()) {
current_postfx_preset = std::clamp( resolvePostFXPresetName();
current_postfx_preset,
0,
static_cast<int>(postfx_presets.size()) - 1);
} else { } else {
current_postfx_preset = 0; video.shader.current_postfx_preset = 0;
} }
std::cout << "PostFX file loaded: " << postfx_presets.size() << " preset(s)\n"; std::cout << "PostFX file loaded: " << postfx_presets.size() << " preset(s)\n";
@@ -950,7 +1011,7 @@ namespace Options {
postfx_presets.push_back({"SCANLINES", 0.0F, 0.8F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F}); postfx_presets.push_back({"SCANLINES", 0.0F, 0.8F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F});
postfx_presets.push_back({"SUBTLE", 0.3F, 0.4F, 0.05F, 0.0F, 0.3F, 0.0F, 0.0F, 0.0F}); postfx_presets.push_back({"SUBTLE", 0.3F, 0.4F, 0.05F, 0.0F, 0.3F, 0.0F, 0.0F, 0.0F});
postfx_presets.push_back({"CRT LIVE", 0.5F, 0.6F, 0.3F, 0.3F, 0.4F, 0.3F, 0.4F, 0.8F}); postfx_presets.push_back({"CRT LIVE", 0.5F, 0.6F, 0.3F, 0.3F, 0.4F, 0.3F, 0.4F, 0.8F});
current_postfx_preset = 0; video.shader.current_postfx_preset = 0;
return true; return true;
} }
@@ -981,7 +1042,7 @@ namespace Options {
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({"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({"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.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});
current_crtpi_preset = 0; video.shader.current_crtpi_preset = 0;
return true; return true;
} }
out << "# JailDoctor's Dilemma - CrtPi Shader Presets\n"; out << "# JailDoctor's Dilemma - CrtPi Shader Presets\n";
@@ -1062,7 +1123,7 @@ namespace Options {
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({"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({"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.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});
current_crtpi_preset = 0; video.shader.current_crtpi_preset = 0;
return true; return true;
} }
@@ -1121,13 +1182,11 @@ namespace Options {
} }
} }
// Resolver el nombre del preset a índice
if (!crtpi_presets.empty()) { if (!crtpi_presets.empty()) {
current_crtpi_preset = std::clamp( resolveCrtPiPresetName();
current_crtpi_preset,
0,
static_cast<int>(crtpi_presets.size()) - 1);
} else { } else {
current_crtpi_preset = 0; video.shader.current_crtpi_preset = 0;
} }
std::cout << "CrtPi file loaded: " << crtpi_presets.size() << " preset(s)\n"; std::cout << "CrtPi file loaded: " << crtpi_presets.size() << " preset(s)\n";
@@ -1138,7 +1197,7 @@ namespace Options {
// Cargar defaults en memoria en caso de error // Cargar defaults en memoria en caso de error
crtpi_presets.clear(); 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({"DEFAULT", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false});
current_crtpi_preset = 0; video.shader.current_crtpi_preset = 0;
return false; return false;
} }
} }

View File

@@ -75,21 +75,43 @@ namespace Options {
float height{Defaults::Border::HEIGHT}; // Alto del borde float height{Defaults::Border::HEIGHT}; // Alto del borde
}; };
// Estructura para las opciones de video // Estructura para las opciones de GPU
struct Video { struct GPU {
bool fullscreen{Defaults::Video::FULLSCREEN}; // Contiene el valor del modo de pantalla completa bool acceleration{Defaults::Video::GPU_ACCELERATION}; // Usar aceleración GPU; false = path SDL fallback directo
Screen::Filter filter{Defaults::Video::FILTER}; // Filtro usado para el escalado de la imagen std::string preferred_driver; // Driver GPU preferido; vacío = auto. Aplica en el próximo arranque.
bool vertical_sync{Defaults::Video::VERTICAL_SYNC}; // Indica si se quiere usar vsync o no };
bool postfx{Defaults::Video::POSTFX}; // Indica si se van a usar efectos PostFX o no
bool supersampling{Defaults::Video::SUPERSAMPLING}; // Indica si el supersampling 3× está activo // Estructura para las opciones de supersampling
bool integer_scale{Defaults::Video::INTEGER_SCALE}; // Indica si el escalado de la imagen ha de ser entero en el modo a pantalla completa struct Supersampling {
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 bool enabled{Defaults::Video::SUPERSAMPLING}; // Indica si el supersampling 3× está activo
bool linear_upscale{Defaults::Video::LINEAR_UPSCALE}; // Upscale LINEAR (true) o NEAREST (false) bool linear_upscale{Defaults::Video::LINEAR_UPSCALE}; // Upscale LINEAR (true) o NEAREST (false)
int downscale_algo{Defaults::Video::DOWNSCALE_ALGO}; // 0=bilinear, 1=Lanczos2, 2=Lanczos3 int downscale_algo{Defaults::Video::DOWNSCALE_ALGO}; // 0=bilinear, 1=Lanczos2, 2=Lanczos3
Border border{}; // Borde de la pantalla };
std::string palette{Defaults::Video::PALETTE_NAME}; // Paleta de colores a usar en el juego
std::string info; // Información sobre el modo de deo // Estructura para las opciones de shader (dentro de Video)
std::string gpu_preferred_driver; // Driver GPU preferido; vacío = auto. Aplica en el próximo arranque. struct ShaderConfig {
bool enabled{Defaults::Video::SHADER_ENABLED}; // Indica si se usan shaders de post-procesado
Rendering::ShaderType current_shader{Rendering::ShaderType::POSTFX}; // Shader de post-procesado activo
std::string current_postfx_preset_name; // Nombre del preset PostFX leído del config
std::string current_crtpi_preset_name; // Nombre del preset CrtPi leído del config
int current_postfx_preset{0}; // Índice resuelto del preset PostFX
int current_crtpi_preset{0}; // Índice resuelto del preset CrtPi
};
// Estructura para las opciones de video
struct Video {
bool fullscreen{Defaults::Video::FULLSCREEN}; // Contiene el valor del modo de pantalla completa
Screen::Filter filter{Defaults::Video::FILTER}; // Filtro usado para el escalado de la imagen
bool vertical_sync{Defaults::Video::VERTICAL_SYNC}; // Indica si se quiere usar vsync o no
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 info; // Información sobre el modo de vídeo
Border border{}; // Borde de la pantalla
GPU gpu{}; // Opciones de aceleración GPU
Supersampling supersampling{}; // Opciones de supersampling
ShaderConfig shader{}; // Opciones de shader post-procesado
}; };
// Estructura para las opciones de musica // Estructura para las opciones de musica
@@ -114,9 +136,10 @@ namespace Options {
// Estructura para las opciones de juego // Estructura para las opciones de juego
struct Game { struct Game {
float width{Defaults::Canvas::WIDTH}; // Ancho de la resolucion del juego float width{Defaults::Canvas::WIDTH}; // Ancho de la resolucion del juego
float height{Defaults::Canvas::HEIGHT}; // Alto de la resolucion del juego float height{Defaults::Canvas::HEIGHT}; // Alto de la resolucion del juego
int player_skin{Defaults::Game::Player::SKIN}; // Skin del jugador (1=normal, 2=alternativa) std::string player_skin{Defaults::Game::Player::SKIN}; // Skin del jugador ("default" o nombre de enemigo)
int player_color{Defaults::Game::Player::COLOR}; // Color del jugador (-1 = automático, 0-15 = color fijo)
}; };
// Estructura para un preset de PostFX // Estructura para un preset de PostFX
@@ -171,17 +194,12 @@ namespace Options {
// --- Variables PostFX --- // --- Variables PostFX ---
inline std::vector<PostFXPreset> postfx_presets{}; // Lista de presets de PostFX inline std::vector<PostFXPreset> postfx_presets{}; // Lista de presets de PostFX
inline int current_postfx_preset{0}; // Índice del preset de PostFX actual
inline std::string postfx_file_path{}; // Ruta del fichero postfx.yaml inline std::string postfx_file_path{}; // Ruta del fichero postfx.yaml
// --- Variables CrtPi --- // --- Variables CrtPi ---
inline std::vector<CrtPiPreset> crtpi_presets{}; // Lista de presets del shader CRT-Pi inline std::vector<CrtPiPreset> crtpi_presets{}; // Lista de presets del shader CRT-Pi
inline int current_crtpi_preset{0}; // Índice del preset CRT-Pi actual
inline std::string crtpi_file_path{}; // Ruta del fichero crtpi.yaml inline std::string crtpi_file_path{}; // Ruta del fichero crtpi.yaml
// --- Shader activo ---
inline Rendering::ShaderType current_shader{Rendering::ShaderType::POSTFX}; // Shader de post-procesado activo
// --- Funciones públicas --- // --- Funciones públicas ---
void setConfigFile(const std::string& path); // Establece la ruta del fichero de configuración void setConfigFile(const std::string& path); // Establece la ruta del fichero de configuración
auto loadFromFile() -> bool; // Carga las opciones desde el fichero configurado auto loadFromFile() -> bool; // Carga las opciones desde el fichero configurado
@@ -192,4 +210,7 @@ namespace Options {
void setCrtPiFile(const std::string& path); // Establece la ruta del fichero de CrtPi void setCrtPiFile(const std::string& path); // Establece la ruta del fichero de CrtPi
auto loadCrtPiFromFile() -> bool; // Carga los presets de CrtPi desde el fichero (crea defaults si no existe) auto loadCrtPiFromFile() -> bool; // Carga los presets de CrtPi desde el fichero (crea defaults si no existe)
void resolvePostFXPresetName(); // Resuelve el nombre del preset PostFX a índice
void resolveCrtPiPresetName(); // Resuelve el nombre del preset CrtPi a índice
} // namespace Options } // namespace Options

View File

@@ -391,7 +391,7 @@ void Ending2::placeSprites() const {
const float X = (Options::game.width - sprites_.back()->getWidth()) / 2; const float X = (Options::game.width - sprites_.back()->getWidth()) / 2;
const float Y = sprites_.back()->getPosY() + (sprite_max_height_ * 2); const float Y = sprites_.back()->getPosY() + (sprite_max_height_ * 2);
sprites_.back()->setPos(X, Y); sprites_.back()->setPos(X, Y);
sprites_.back()->setCurrentAnimation("walk"); sprites_.back()->setCurrentAnimation("default");
} }
// Crea los sprites con las texturas con los textos // Crea los sprites con las texturas con los textos

View File

@@ -32,7 +32,8 @@
#include "utils/utils.hpp" // Para PaletteColor, stringToColor #include "utils/utils.hpp" // Para PaletteColor, stringToColor
#ifdef _DEBUG #ifdef _DEBUG
#include "core/system/debug.hpp" // Para Debug #include "core/system/debug.hpp" // Para Debug
#include "game/editor/map_editor.hpp" // Para MapEditor
#endif #endif
// Constructor // Constructor
@@ -64,12 +65,16 @@ Game::Game(Mode mode)
Cheevos::get()->enable(!Options::cheats.enabled()); // Deshabilita los logros si hay trucos activados Cheevos::get()->enable(!Options::cheats.enabled()); // Deshabilita los logros si hay trucos activados
Cheevos::get()->clearUnobtainableState(); Cheevos::get()->clearUnobtainableState();
GameControl::refresh_player_color = [this]() -> void { player_->setColor(); };
Console::get()->on_toggle = [this](bool open) { player_->setIgnoreInput(open); }; Console::get()->on_toggle = [this](bool open) { player_->setIgnoreInput(open); };
if (Console::get()->isActive()) { player_->setIgnoreInput(true); } if (Console::get()->isActive()) { player_->setIgnoreInput(true); }
GameControl::change_player_skin = [this](int skin_num) -> void { GameControl::change_player_skin = [this](const std::string& skin_name) -> void {
Options::game.player_skin = skin_num; Options::game.player_skin = skin_name;
player_->setSkin(skin_num); player_->setSkin(skin_name);
scoreboard_->refreshPlayerSkin();
};
GameControl::change_player_color = [this](int color) -> void {
Options::game.player_color = color;
player_->setColor();
}; };
#ifdef _DEBUG #ifdef _DEBUG
@@ -80,9 +85,17 @@ Game::Game(Mode mode)
Options::stats.items = count; Options::stats.items = count;
}; };
GameControl::toggle_debug_mode = [this]() -> void { GameControl::toggle_debug_mode = [this]() -> void {
const bool ENTERING_DEBUG = !Debug::get()->isEnabled();
if (ENTERING_DEBUG) {
invincible_before_debug_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED);
}
Debug::get()->toggleEnabled(); Debug::get()->toggleEnabled();
room_->redrawMap(); room_->redrawMap();
Options::cheats.invincible = static_cast<Options::Cheat::State>(Debug::get()->isEnabled()); if (Debug::get()->isEnabled()) {
Options::cheats.invincible = Options::Cheat::State::ENABLED;
} else {
Options::cheats.invincible = invincible_before_debug_ ? Options::Cheat::State::ENABLED : Options::Cheat::State::DISABLED;
}
player_->setColor(); player_->setColor();
scoreboard_data_->music = !Debug::get()->isEnabled(); scoreboard_data_->music = !Debug::get()->isEnabled();
scoreboard_data_->music ? Audio::get()->resumeMusic() : Audio::get()->pauseMusic(); scoreboard_data_->music ? Audio::get()->resumeMusic() : Audio::get()->pauseMusic();
@@ -107,6 +120,26 @@ Game::Game(Mode mode)
Debug::get()->saveToFile(); Debug::get()->saveToFile();
return "Pos:" + std::to_string(tile_x) + "," + std::to_string(tile_y); return "Pos:" + std::to_string(tile_x) + "," + std::to_string(tile_y);
}; };
GameControl::enter_editor = [this]() -> void {
MapEditor::get()->enter(room_, player_, current_room_, scoreboard_data_);
};
GameControl::exit_editor = [this]() -> void {
MapEditor::get()->exit();
// Recargar la habitación desde disco (con los cambios del editor)
Resource::Cache::get()->reloadRoom(current_room_);
changeRoom(current_room_);
player_->setRoom(room_);
};
GameControl::revert_editor = []() -> std::string {
return MapEditor::get()->revert();
};
GameControl::get_adjacent_room = [this](const std::string& direction) -> std::string {
if (direction == "UP") { return room_->getRoom(Room::Border::TOP); }
if (direction == "DOWN") { return room_->getRoom(Room::Border::BOTTOM); }
if (direction == "LEFT") { return room_->getRoom(Room::Border::LEFT); }
if (direction == "RIGHT") { return room_->getRoom(Room::Border::RIGHT); }
return "0";
};
#endif #endif
SceneManager::current = (mode_ == Mode::GAME) ? SceneManager::Scene::GAME : SceneManager::Scene::DEMO; SceneManager::current = (mode_ == Mode::GAME) ? SceneManager::Scene::GAME : SceneManager::Scene::DEMO;
@@ -116,8 +149,8 @@ Game::Game(Mode mode)
Game::~Game() { Game::~Game() {
ItemTracker::destroy(); ItemTracker::destroy();
GameControl::refresh_player_color = nullptr;
GameControl::change_player_skin = nullptr; GameControl::change_player_skin = nullptr;
GameControl::change_player_color = nullptr;
Console::get()->on_toggle = nullptr; Console::get()->on_toggle = nullptr;
#ifdef _DEBUG #ifdef _DEBUG
@@ -127,6 +160,12 @@ Game::~Game() {
GameControl::toggle_debug_mode = nullptr; GameControl::toggle_debug_mode = nullptr;
GameControl::set_initial_room = nullptr; GameControl::set_initial_room = nullptr;
GameControl::set_initial_pos = nullptr; GameControl::set_initial_pos = nullptr;
if (MapEditor::get()->isActive()) { MapEditor::get()->exit(); }
GameControl::enter_editor = nullptr;
GameControl::exit_editor = nullptr;
GameControl::revert_editor = nullptr;
GameControl::reload_current_room = nullptr;
GameControl::get_adjacent_room = nullptr;
#endif #endif
} }
@@ -136,8 +175,27 @@ void Game::handleEvents() {
while (SDL_PollEvent(&event)) { while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event); GlobalEvents::handle(event);
#ifdef _DEBUG #ifdef _DEBUG
// En modo editor: click del ratón cierra la consola
if (Console::get()->isActive() && MapEditor::get()->isActive() &&
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
Console::get()->toggle();
}
if (!Console::get()->isActive()) { if (!Console::get()->isActive()) {
handleDebugEvents(event); // Tecla 9: toggle editor (funciona tanto dentro como fuera del editor)
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_9 && static_cast<int>(event.key.repeat) == 0) {
if (MapEditor::get()->isActive()) {
GameControl::exit_editor();
Notifier::get()->show({Locale::get()->get("game.editor_disabled")}); // NOLINT(readability-static-accessed-through-instance)
} else {
GameControl::enter_editor();
Notifier::get()->show({Locale::get()->get("game.editor_enabled")}); // NOLINT(readability-static-accessed-through-instance)
}
} else if (MapEditor::get()->isActive()) {
MapEditor::get()->handleEvent(event);
} else {
handleDebugEvents(event);
}
} }
#endif #endif
} }
@@ -160,6 +218,14 @@ void Game::handleInput() {
return; return;
} }
#ifdef _DEBUG
// Si el editor de mapas está activo, no procesar inputs del juego
if (MapEditor::get()->isActive()) {
GlobalInputs::handle();
return;
}
#endif
// Durante fade/postfade, solo procesar inputs globales // Durante fade/postfade, solo procesar inputs globales
if (state_ != State::PLAYING) { if (state_ != State::PLAYING) {
GlobalInputs::handle(); GlobalInputs::handle();
@@ -228,6 +294,14 @@ void Game::update() {
// Actualiza el juego en estado PLAYING // Actualiza el juego en estado PLAYING
void Game::updatePlaying(float delta_time) { void Game::updatePlaying(float delta_time) {
#ifdef _DEBUG
// Si el editor de mapas está activo, delegar en él y no ejecutar gameplay
if (MapEditor::get()->isActive()) {
MapEditor::get()->update(delta_time);
return;
}
#endif
// Actualiza los objetos // Actualiza los objetos
room_->update(delta_time); room_->update(delta_time);
switch (mode_) { switch (mode_) {
@@ -367,8 +441,19 @@ void Game::renderPlaying() {
// Prepara para dibujar el frame // Prepara para dibujar el frame
Screen::get()->start(); Screen::get()->start();
// Dibuja los elementos del juego en orden // Dibuja el mapa de tiles (siempre)
room_->renderMap(); room_->renderMap();
#ifdef _DEBUG
// Si el editor está activo, delegar el renderizado de entidades y statusbar
if (MapEditor::get()->isActive()) {
MapEditor::get()->render();
Screen::get()->render();
return;
}
#endif
// Dibuja los elementos del juego en orden
room_->renderEnemies(); room_->renderEnemies();
room_->renderItems(); room_->renderItems();
if (mode_ == Mode::GAME) { if (mode_ == Mode::GAME) {
@@ -512,15 +597,24 @@ void Game::handleDebugEvents(const SDL_Event& event) { // NOLINT(readability-co
Notifier::get()->show({Locale::get()->get("achievements.header"), Locale::get()->get("achievements.c11")}, Notifier::Style::CHEEVO, -1, false, "F7"); // NOLINT(readability-static-accessed-through-instance) Notifier::get()->show({Locale::get()->get("achievements.header"), Locale::get()->get("achievements.c11")}, Notifier::Style::CHEEVO, -1, false, "F7"); // NOLINT(readability-static-accessed-through-instance)
break; break;
case SDLK_0: case SDLK_0: {
const bool ENTERING_DEBUG = !Debug::get()->isEnabled();
if (ENTERING_DEBUG) {
invincible_before_debug_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED);
}
Debug::get()->toggleEnabled(); Debug::get()->toggleEnabled();
Notifier::get()->show({Debug::get()->isEnabled() ? Locale::get()->get("game.debug_enabled") : Locale::get()->get("game.debug_disabled")}); // NOLINT(readability-static-accessed-through-instance) Notifier::get()->show({Debug::get()->isEnabled() ? Locale::get()->get("game.debug_enabled") : Locale::get()->get("game.debug_disabled")}); // NOLINT(readability-static-accessed-through-instance)
room_->redrawMap(); room_->redrawMap();
Options::cheats.invincible = static_cast<Options::Cheat::State>(Debug::get()->isEnabled()); if (Debug::get()->isEnabled()) {
Options::cheats.invincible = Options::Cheat::State::ENABLED;
} else {
Options::cheats.invincible = invincible_before_debug_ ? Options::Cheat::State::ENABLED : Options::Cheat::State::DISABLED;
}
player_->setColor(); player_->setColor();
scoreboard_data_->music = !Debug::get()->isEnabled(); scoreboard_data_->music = !Debug::get()->isEnabled();
scoreboard_data_->music ? Audio::get()->resumeMusic() : Audio::get()->pauseMusic(); scoreboard_data_->music ? Audio::get()->resumeMusic() : Audio::get()->pauseMusic();
break; break;
}
default: default:
break; break;
@@ -638,6 +732,9 @@ auto Game::changeRoom(const std::string& room_path) -> bool {
// Pasa la nueva habitación al jugador // Pasa la nueva habitación al jugador
player_->setRoom(room_); player_->setRoom(room_);
// Recalcula el color del jugador (evita coincidir con el fondo)
player_->setColor();
// Cambia la habitación actual // Cambia la habitación actual
current_room_ = room_path; current_room_ = room_path;
@@ -884,7 +981,7 @@ void Game::checkEndGameCheevos() { // NOLINT(readability-convert-member-functio
// Inicializa al jugador // Inicializa al jugador
void Game::initPlayer(const Player::SpawnData& spawn_point, std::shared_ptr<Room> room) { // NOLINT(readability-convert-member-functions-to-static) void Game::initPlayer(const Player::SpawnData& spawn_point, std::shared_ptr<Room> room) { // NOLINT(readability-convert-member-functions-to-static)
const bool IGNORE_INPUT = player_ != nullptr && player_->getIgnoreInput(); const bool IGNORE_INPUT = player_ != nullptr && player_->getIgnoreInput();
std::string player_animations = (Options::game.player_skin == 2) ? "player2.yaml" : "player.yaml"; std::string player_animations = Player::skinToAnimationPath(Options::game.player_skin);
const Player::Data PLAYER{.spawn_data = spawn_point, .animations_path = player_animations, .room = std::move(room)}; const Player::Data PLAYER{.spawn_data = spawn_point, .animations_path = player_animations, .room = std::move(room)};
player_ = std::make_shared<Player>(PLAYER); player_ = std::make_shared<Player>(PLAYER);
if (IGNORE_INPUT) { player_->setIgnoreInput(true); } if (IGNORE_INPUT) { player_->setIgnoreInput(true); }

View File

@@ -130,5 +130,7 @@ class Game {
// Variables de debug para arrastre con ratón // Variables de debug para arrastre con ratón
bool debug_dragging_player_{false}; // Indica si estamos arrastrando al jugador con el ratón bool debug_dragging_player_{false}; // Indica si estamos arrastrando al jugador con el ratón
float debug_drag_speed_{0.0F}; // Velocidad actual del arrastre (ease-in) float debug_drag_speed_{0.0F}; // Velocidad actual del arrastre (ease-in)
// Estado previo de invencibilidad antes de entrar en modo debug
bool invincible_before_debug_{false};
#endif #endif
}; };

View File

@@ -2,43 +2,21 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cctype> // Para toupper #include <algorithm> // Para ranges::transform
#include <functional> // Para function #include <cctype> // Para toupper
#include <iostream> // Para std::cout #include <sstream> // Para std::istringstream
#include <sstream> // Para std::istringstream #include <string> // Para string
#include <string> // Para string #include <vector> // Para vector
#include <vector> // Para vector
#include "core/audio/audio.hpp" // Para Audio
#include "core/locale/locale.hpp" // Para Locale
#include "core/rendering/render_info.hpp" // Para RenderInfo
#include "core/rendering/screen.hpp" // Para Screen #include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/sprite/sprite.hpp" // Para Sprite #include "core/rendering/sprite/sprite.hpp" // Para Sprite
#include "core/rendering/surface.hpp" // Para Surface #include "core/rendering/surface.hpp" // Para Surface
#include "core/rendering/text.hpp" // Para Text #include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource #include "core/resources/resource_cache.hpp" // Para Resource
#include "game/game_control.hpp" // Para GameControl (refresh_player_color)
#include "game/options.hpp" // Para Options #include "game/options.hpp" // Para Options
#include "game/scene_manager.hpp" // Para SceneManager
#include "game/ui/notifier.hpp" // Para Notifier #include "game/ui/notifier.hpp" // Para Notifier
#ifdef _DEBUG // ── Helpers de texto ──────────────────────────────────────────────────────────
#include "core/system/debug.hpp" // Para Debug
#endif
// ── Sistema de comandos ────────────────────────────────────────────────────────
// Mapa de completions: {ruta_completa_en_mayúsculas, {opciones}}
// Ej: {"CHEAT OPEN THE", {"JAIL"}}
using CompletionMap = std::vector<std::pair<std::string_view, std::vector<std::string_view>>>;
struct ConsoleCommand {
std::string_view keyword;
std::function<std::string(const std::vector<std::string>& args)> execute;
bool instant{false}; // Si true, muestra la respuesta sin efecto typewriter
bool hidden{false}; // Si true, no aparece en el autocompletado (TAB)
CompletionMap completions{}; // Árbol de sub-argumentos para TAB; cargado en el constructor de Console
};
// Convierte la entrada a uppercase y la divide en tokens por espacios // Convierte la entrada a uppercase y la divide en tokens por espacios
static auto parseTokens(const std::string& input) -> std::vector<std::string> { static auto parseTokens(const std::string& input) -> std::vector<std::string> {
@@ -60,854 +38,6 @@ static auto parseTokens(const std::string& input) -> std::vector<std::string> {
return tokens; return tokens;
} }
// Macro para comando de toggle booleano (evita repetición en ON/OFF)
#define BOOL_TOGGLE_CMD(label, getter, toggle_fn) \
[](const std::vector<std::string>& args) -> std::string { \
if (args.empty()) { \
(toggle_fn); \
return label " " + std::string((getter) ? "ON" : "OFF"); \
} \
if (args[0] == "ON") { \
if (getter) { return label " already ON"; } \
(toggle_fn); \
return label " ON"; \
} \
if (args[0] == "OFF") { \
if (!(getter)) { return label " already OFF"; } \
(toggle_fn); \
return label " OFF"; \
} \
return "usage: " label " [on|off]"; \
}
// Texto de ayuda común para HELP y ?
static void printHelp() {
std::cout << "=== JDD CONSOLE COMMANDS ===" << '\n';
std::cout << '\n';
std::cout << "[VIDEO]" << '\n';
std::cout << " SS [ON|OFF|SIZE] Supersampling" << '\n';
std::cout << " SS UPSCALE [NEAREST|LINEAR] SS upscale filter" << '\n';
std::cout << " SS DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3] SS downscale algorithm" << '\n';
std::cout << " SHADER [ON|OFF|NEXT [PRESET]|POSTFX|CRTPI] Toggle/select shader (F4)" << '\n';
std::cout << " BORDER [ON|OFF] Decorative border (B)" << '\n';
std::cout << " FULLSCREEN [ON|OFF] Fullscreen mode (F3)" << '\n';
std::cout << " ZOOM [UP|DOWN] Window zoom (F1/F2)" << '\n';
std::cout << " INTSCALE [ON|OFF] Integer scaling (F7)" << '\n';
std::cout << " VSYNC [ON|OFF] Vertical sync" << '\n';
std::cout << " DRIVER [LIST|AUTO|NONE|<name>] GPU driver (restart to apply)" << '\n';
std::cout << " PALETTE [NEXT|PREV] Color palette (F5/F6)" << '\n';
std::cout << '\n';
std::cout << "[AUDIO]" << '\n';
std::cout << " AUDIO [ON|OFF|VOL <0-100>] Audio master" << '\n';
std::cout << " MUSIC [ON|OFF|VOL <0-100>] Music volume" << '\n';
std::cout << " SOUND [ON|OFF|VOL <0-100>] Sound volume" << '\n';
std::cout << '\n';
std::cout << "[GAME]" << '\n';
std::cout << " SET PLAYER SKIN <1|2> Change player skin (GAME only)" << '\n';
std::cout << " RESTART Restart from the beginning" << '\n';
std::cout << " KIOSK [ON] Enable kiosk mode" << '\n';
std::cout << " EXIT / QUIT Quit application" << '\n';
std::cout << '\n';
std::cout << "[INFO]" << '\n';
std::cout << " SHOW [INFO] Show info overlay" << '\n';
std::cout << " HIDE [INFO] Hide info overlay" << '\n';
std::cout << " SIZE Window size in pixels" << '\n';
std::cout << " HELP / ? Show this help in terminal" << '\n';
#ifdef _DEBUG
std::cout << '\n';
std::cout << "[DEBUG]" << '\n';
std::cout << " DEBUG Toggle debug overlay (F12)" << '\n';
std::cout << " ROOM <1-60>|NEXT|PREV Change to room number (GAME only)" << '\n';
std::cout << " SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART]" << '\n';
std::cout << " SET INITIAL [ROOM|POS] Set initial room/position from current state (GAME only)" << '\n';
std::cout << " SET INITIAL SCENE [<name>] Set initial debug scene (GAME|LOGO|TITLE|LOADING|CREDITS|ENDING|ENDING2)" << '\n';
std::cout << " SET ITEMS <0-200> Set collected items count (GAME only)" << '\n';
std::cout << " CHEAT INFINITE LIVES [ON|OFF] Infinite lives (GAME only)" << '\n';
std::cout << " CHEAT INVINCIBILITY [ON|OFF] Invincibility (GAME only)" << '\n';
std::cout << " CHEAT OPEN THE JAIL Open the jail (GAME only)" << '\n';
std::cout << " CHEAT CLOSE THE JAIL Close the jail (GAME only)" << '\n';
std::cout << " SHOW NOTIFICATION Test notification popup" << '\n';
std::cout << " SHOW CHEEVO Test achievement notification" << '\n';
#endif
}
// En Release, los comandos de truco (CHEAT) son ocultos en el autocompletado
#ifdef _DEBUG
static constexpr bool CHEAT_HIDDEN = false;
#else
static constexpr bool CHEAT_HIDDEN = true;
#endif
// Tabla de comandos disponibles
static const std::vector<ConsoleCommand> COMMANDS = {
// SS [ON|OFF|SIZE|UPSCALE [NEAREST|LINEAR]|DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]] — Supersampling
{.keyword = "SS", .execute = [](const std::vector<std::string>& args) -> std::string {
static const std::array<std::string_view, 3> DOWNSCALE_NAMES = {"Bilinear", "Lanczos2", "Lanczos3"};
if (!args.empty() && args[0] == "SIZE") {
if (!Options::video.supersampling) { return "Supersampling is OFF: no texture"; }
const auto [w, h] = Screen::get()->getSsTextureSize();
if (w == 0) { return "SS texture: not active"; }
return "SS texture: " + std::to_string(w) + "x" + std::to_string(h);
}
if (!args.empty() && args[0] == "UPSCALE") {
if (args.size() == 1) {
Screen::get()->setLinearUpscale(!Options::video.linear_upscale);
return std::string("Upscale: ") + (Options::video.linear_upscale ? "Linear" : "Nearest");
}
if (args[1] == "NEAREST") {
if (!Options::video.linear_upscale) { return "Upscale already Nearest"; }
Screen::get()->setLinearUpscale(false);
return "Upscale: Nearest";
}
if (args[1] == "LINEAR") {
if (Options::video.linear_upscale) { return "Upscale already Linear"; }
Screen::get()->setLinearUpscale(true);
return "Upscale: Linear";
}
return "usage: ss upscale [nearest|linear]";
}
if (!args.empty() && args[0] == "DOWNSCALE") {
if (args.size() == 1) {
return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast<size_t>(Options::video.downscale_algo)]);
}
int algo = -1;
if (args[1] == "BILINEAR") { algo = 0; }
if (args[1] == "LANCZOS2") { algo = 1; }
if (args[1] == "LANCZOS3") { algo = 2; }
if (algo == -1) { return "usage: ss downscale [bilinear|lanczos2|lanczos3]"; }
if (Options::video.downscale_algo == algo) {
return std::string("Downscale already ") + std::string(DOWNSCALE_NAMES[static_cast<size_t>(algo)]);
}
Screen::get()->setDownscaleAlgo(algo);
return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast<size_t>(algo)]);
}
if (args.empty()) {
Screen::get()->toggleSupersampling();
return std::string("PostFX Supersampling ") + (Options::video.supersampling ? "ON" : "OFF");
}
if (args[0] == "ON") {
if (Options::video.supersampling) { return "Supersampling already ON"; }
Screen::get()->toggleSupersampling();
return "PostFX Supersampling ON";
}
if (args[0] == "OFF") {
if (!Options::video.supersampling) { return "Supersampling already OFF"; }
Screen::get()->toggleSupersampling();
return "PostFX Supersampling OFF";
}
return "usage: ss [on|off|size|upscale [nearest|linear]|downscale [bilinear|lanczos2|lanczos3]]";
},
.completions = {
{"SS", {"ON", "OFF", "SIZE", "UPSCALE", "DOWNSCALE"}},
{"SS UPSCALE", {"NEAREST", "LINEAR"}},
{"SS DOWNSCALE", {"BILINEAR", "LANCZOS2", "LANCZOS3"}},
}},
// SHADER [ON|OFF|NEXT [PRESET]|POSTFX|CRTPI] — Toggle/cicla/selecciona shader (F4 / Shift+F4)
{.keyword = "SHADER", .execute = [](const std::vector<std::string>& args) -> std::string {
if (args.empty()) {
Screen::get()->toggleShaders();
return std::string("Shader ") + (Options::video.postfx ? "ON" : "OFF");
}
if (args[0] == "ON") {
if (Options::video.postfx) { return "Shader already ON"; }
Screen::get()->toggleShaders();
return "Shader ON";
}
if (args[0] == "OFF") {
if (!Options::video.postfx) { return "Shader already OFF"; }
Screen::get()->toggleShaders();
return "Shader OFF";
}
if (args[0] == "POSTFX") {
Screen::get()->setActiveShader(Rendering::ShaderType::POSTFX);
return "Shader: PostFX";
}
if (args[0] == "CRTPI") {
Screen::get()->setActiveShader(Rendering::ShaderType::CRTPI);
return "Shader: CrtPi";
}
if (args[0] == "NEXT") {
// SHADER NEXT PRESET → cicla presets del shader activo
if (args.size() >= 2 && args[1] == "PRESET") {
if (Options::current_shader == Rendering::ShaderType::CRTPI) {
if (Options::crtpi_presets.empty()) { return "No CrtPi presets available"; }
Options::current_crtpi_preset =
(Options::current_crtpi_preset + 1) %
static_cast<int>(Options::crtpi_presets.size());
Screen::get()->reloadCrtPi();
return "CrtPi preset: " +
Options::crtpi_presets[static_cast<size_t>(Options::current_crtpi_preset)].name;
}
if (Options::postfx_presets.empty()) { return "No PostFX presets available"; }
Options::current_postfx_preset =
(Options::current_postfx_preset + 1) %
static_cast<int>(Options::postfx_presets.size());
Screen::get()->reloadPostFX();
return "PostFX preset: " +
Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)].name;
}
// SHADER NEXT → cicla entre tipos de shader (PostFX ↔ CrtPi)
Screen::get()->nextShader();
return std::string("Shader: ") +
(Options::current_shader == Rendering::ShaderType::CRTPI ? "CrtPi" : "PostFX");
}
return "usage: shader [on|off|next [preset]|postfx|crtpi]";
},
.completions = {
{"SHADER", {"ON", "OFF", "NEXT", "POSTFX", "CRTPI"}},
{"SHADER NEXT", {"PRESET"}},
}},
// BORDER [ON|OFF] — Borde decorativo (B)
{.keyword = "BORDER", .execute = BOOL_TOGGLE_CMD("Border", Options::video.border.enabled, Screen::get()->toggleBorder()), .completions = {{"BORDER", {"ON", "OFF"}}}},
// FULLSCREEN [ON|OFF [PLEASE]] — Pantalla completa (F3); OFF bloqueado en kiosk sin PLEASE
{.keyword = "FULLSCREEN", .execute = [](const std::vector<std::string>& args) -> std::string {
const bool EXPLICIT_ON = !args.empty() && args[0] == "ON";
const bool EXPLICIT_OFF = !args.empty() && args[0] == "OFF";
const bool WITH_PLEASE = !args.empty() && args.back() == "PLEASE";
const bool IS_TOGGLE = args.empty();
const bool TURNING_OFF = EXPLICIT_OFF || (IS_TOGGLE && Options::video.fullscreen);
if (TURNING_OFF && Options::kiosk.enabled && !WITH_PLEASE) {
return "Not allowed in kiosk mode";
}
if (EXPLICIT_ON) {
if (Options::video.fullscreen) { return "Fullscreen already ON"; }
Screen::get()->toggleVideoMode();
return "Fullscreen ON";
}
if (EXPLICIT_OFF) {
if (!Options::video.fullscreen) { return "Fullscreen already OFF"; }
Screen::get()->toggleVideoMode();
return "Fullscreen OFF";
}
if (IS_TOGGLE) {
Screen::get()->toggleVideoMode();
return std::string("Fullscreen ") + (Options::video.fullscreen ? "ON" : "OFF");
}
return "usage: fullscreen [on|off]";
},
.completions = {{"FULLSCREEN", {"ON", "OFF"}}}},
// ZOOM UP/DOWN — Zoom de ventana (F1/F2)
{.keyword = "ZOOM", .execute = [](const std::vector<std::string>& args) -> std::string {
if (args.empty()) { return "usage: zoom [up|down|<1-" + std::to_string(Screen::get()->getMaxZoom()) + ">]"; }
if (args[0] == "UP") {
if (!Screen::get()->incWindowZoom()) { return "Max zoom reached"; }
return "Zoom " + std::to_string(Options::window.zoom);
}
if (args[0] == "DOWN") {
if (!Screen::get()->decWindowZoom()) { return "Min zoom reached"; }
return "Zoom " + std::to_string(Options::window.zoom);
}
// Zoom numérico directo
try {
const int N = std::stoi(args[0]);
const int MAX = Screen::get()->getMaxZoom();
if (N < 1 || N > MAX) {
return "Zoom must be between 1 and " + std::to_string(MAX);
}
if (N == Options::window.zoom) { return "Zoom already " + std::to_string(N); }
Screen::get()->setWindowZoom(N);
return "Zoom " + std::to_string(Options::window.zoom);
} catch (...) {}
return "usage: zoom [up|down|<1-" + std::to_string(Screen::get()->getMaxZoom()) + ">]";
},
.completions = {{"ZOOM", {"UP", "DOWN"}}}},
// INTSCALE [ON|OFF] — Escalado entero (F7)
{.keyword = "INTSCALE", .execute = [](const std::vector<std::string>& args) -> std::string {
const bool ON = args.empty() ? !Options::video.integer_scale
: (args[0] == "ON");
if (!args.empty() && args[0] != "ON" && args[0] != "OFF") {
return "usage: intscale [on|off]";
}
if (ON == Options::video.integer_scale) {
return std::string("IntScale already ") + (ON ? "ON" : "OFF");
}
Screen::get()->toggleIntegerScale();
Screen::get()->setVideoMode(Options::video.fullscreen);
return std::string("IntScale ") + (Options::video.integer_scale ? "ON" : "OFF");
},
.completions = {{"INTSCALE", {"ON", "OFF"}}}},
// VSYNC [ON|OFF] — Sincronización vertical
{.keyword = "VSYNC", .execute = BOOL_TOGGLE_CMD("VSync", Options::video.vertical_sync, Screen::get()->toggleVSync()), .completions = {{"VSYNC", {"ON", "OFF"}}}},
// DRIVER [LIST|AUTO|<nombre>] — Driver GPU (aplica en el próximo arranque)
{.keyword = "DRIVER", .execute = [](const std::vector<std::string>& args) -> std::string {
// Sin argumentos: muestra el driver activo (permitido en kiosk)
if (args.empty()) {
const auto& driver = Screen::get()->getGPUDriver();
return "GPU: " + (driver.empty() ? std::string("sdl") : driver);
}
// LIST: lista drivers disponibles marcando el activo con * (permitido en kiosk)
if (args[0] == "LIST") {
const int COUNT = SDL_GetNumGPUDrivers();
if (COUNT <= 0) { return "No GPU drivers found"; }
const std::string& active = Screen::get()->getGPUDriver();
std::string result = "Drivers:";
for (int i = 0; i < COUNT; ++i) {
const char* name = SDL_GetGPUDriver(i);
if (name != nullptr) {
result += ' ';
result += name;
if (active == name) { result += '*'; }
}
}
SDL_Log("SDL GPU drivers: %s", result.c_str());
return result;
}
// Cambiar driver: bloqueado en kiosk salvo PLEASE
const bool HAS_PLEASE = std::ranges::find(args, std::string("PLEASE")) != args.end();
if (Options::kiosk.enabled && !HAS_PLEASE) {
return "Not allowed in kiosk mode";
}
if (args[0] == "AUTO") {
Options::video.gpu_preferred_driver.clear();
Options::saveToFile();
return "Driver: auto (restart)";
}
if (args[0] == "NONE") {
Options::video.gpu_preferred_driver = "none";
Options::saveToFile();
return "Driver: none (SDL fallback, restart)";
}
std::string driver_lower = args[0];
std::ranges::transform(driver_lower, driver_lower.begin(), ::tolower);
// Validar que el nombre existe en la lista de drivers SDL
const int COUNT = SDL_GetNumGPUDrivers();
bool found = false;
for (int i = 0; i < COUNT && !found; ++i) {
const char* name = SDL_GetGPUDriver(i);
if (name != nullptr && driver_lower == name) { found = true; }
}
if (!found) {
return "Unknown driver: " + driver_lower + ". Use DRIVER LIST or NONE";
}
Options::video.gpu_preferred_driver = driver_lower;
Options::saveToFile();
return "Driver: " + driver_lower + " (restart)";
},
.completions = {{"DRIVER", {"LIST", "AUTO", "NONE"}}}},
// PALETTE NEXT/PREV/<nombre> — Paleta de colores (F5/F6 o por nombre)
{.keyword = "PALETTE", .execute = [](const std::vector<std::string>& args) -> std::string {
const auto palName = []() -> std::string {
std::string name = Options::video.palette;
std::ranges::transform(name, name.begin(), ::tolower);
return name;
};
if (args.empty()) { return "usage: palette [next|prev|<name>]"; }
if (args[0] == "NEXT") {
Screen::get()->nextPalette();
return "Palette: " + palName();
}
if (args[0] == "PREV") {
Screen::get()->previousPalette();
return "Palette: " + palName();
}
if (!Screen::get()->setPaletteByName(args[0])) {
std::string arg_lower = args[0];
std::ranges::transform(arg_lower, arg_lower.begin(), ::tolower);
return "Unknown palette: " + arg_lower;
}
return "Palette: " + palName();
}},
#ifdef _DEBUG
// DEBUG [ON|OFF] — Activa/desactiva el modo debug del juego (tecla 0); solo en escena GAME
{.keyword = "DEBUG", .execute = [](const std::vector<std::string>& args) -> std::string {
if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
if (!GameControl::toggle_debug_mode) { return "Game not initialized"; }
const bool ENABLED = Debug::get()->isEnabled();
if (!args.empty() && args[0] == "ON") {
if (ENABLED) { return "Debug mode already ON"; }
GameControl::toggle_debug_mode();
return "Debug mode ON";
}
if (!args.empty() && args[0] == "OFF") {
if (!ENABLED) { return "Debug mode already OFF"; }
GameControl::toggle_debug_mode();
return "Debug mode OFF";
}
if (!args.empty()) { return "usage: debug [on|off]"; }
GameControl::toggle_debug_mode();
return std::string("Debug mode ") + (Debug::get()->isEnabled() ? "ON" : "OFF");
},
.completions = {{"DEBUG", {"ON", "OFF"}}}},
// ROOM <num>|NEXT|PREV — Cambia a la habitación indicada (1-60); solo en escena GAME
{.keyword = "ROOM", .execute = [](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 <1-60>|next|prev"; }
int num = 0;
if (args[0] == "NEXT" || args[0] == "PREV") {
if (!GameControl::get_current_room) { return "Game not initialized"; }
const std::string current = GameControl::get_current_room();
try {
num = std::stoi(current.substr(0, current.find('.')));
} catch (...) { return "Cannot determine current room"; }
num += (args[0] == "NEXT") ? 1 : -1;
} else {
try {
num = std::stoi(args[0]);
} catch (...) { return "usage: room <1-60>|next|prev"; }
}
if (num < 1 || num > 60) { return "Room must be between 1 and 60"; }
char buf[16];
std::snprintf(buf, sizeof(buf), "%02d.yaml", num);
if (GameControl::change_room && GameControl::change_room(buf)) {
return std::string("Room: ") + buf;
}
return std::string("Room not found: ") + buf;
},
.completions = {{"ROOM", {"NEXT", "PREV"}}}},
#endif
// SHOW INFO — disponible en Release; SHOW NOTIFICATION / SHOW CHEEVO — solo en Debug
{.keyword = "SHOW", .execute = [](const std::vector<std::string>& args) -> std::string {
#ifdef _DEBUG
if (!args.empty() && args[0] == "NOTIFICATION") {
Notifier::get()->show({"NOTIFICATION"});
return "Notification shown";
}
if (!args.empty() && args[0] == "CHEEVO") {
Notifier::get()->show({Locale::get()->get("achievements.header"), Locale::get()->get("achievements.c1")}, Notifier::Style::CHEEVO, -1, false); // NOLINT(readability-static-accessed-through-instance)
return "Cheevo notification shown";
}
if (args.empty() || args[0] != "INFO") { return "usage: show [info|notification|cheevo]"; }
#else
if (args.empty() || args[0] != "INFO") { return "usage: show [info]"; }
#endif
if (RenderInfo::get()->isActive()) { return "Info overlay already ON"; }
RenderInfo::get()->toggle();
return "Info overlay ON";
},
.completions = {
#ifdef _DEBUG
{"SHOW", {"INFO", "NOTIFICATION", "CHEEVO"}},
#else
{"SHOW", {"INFO"}},
#endif
}},
// HIDE INFO — disponible en Release
{.keyword = "HIDE", .execute = [](const std::vector<std::string>& args) -> std::string {
if (args.empty() || args[0] != "INFO") { return "usage: hide [info]"; }
if (!RenderInfo::get()->isActive()) { return "Info overlay already OFF"; }
RenderInfo::get()->toggle();
return "Info overlay OFF";
},
.completions = {{"HIDE", {"INFO"}}}},
// CHEAT <subcomando> — Trucos de juego; solo en escena GAME; no aparece en ayuda en builds Release
{.keyword = "CHEAT", .execute = [](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|open the jail|close the jail]"; }
// 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());
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]";
}
if (GameControl::refresh_player_color) { GameControl::refresh_player_color(); }
return std::string("Infinite lives ") + (cheat == State::ENABLED ? "ON" : "OFF");
}
// 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]";
}
if (GameControl::refresh_player_color) { GameControl::refresh_player_color(); }
return std::string("Invincibility ") + (cheat == State::ENABLED ? "ON" : "OFF");
}
// CHEAT OPEN THE JAIL
if (args[0] == "OPEN") {
if (args.size() < 3 || args[1] != "THE" || args[2] != "JAIL") { return "usage: cheat open the jail"; }
if (Options::cheats.jail_is_open == Options::Cheat::State::ENABLED) { return "Jail already open"; }
Options::cheats.jail_is_open = Options::Cheat::State::ENABLED;
return "Jail opened";
}
// CHEAT CLOSE THE JAIL
if (args[0] == "CLOSE") {
if (args.size() < 3 || args[1] != "THE" || args[2] != "JAIL") { return "usage: cheat close the jail"; }
if (Options::cheats.jail_is_open == Options::Cheat::State::DISABLED) { return "Jail already closed"; }
Options::cheats.jail_is_open = Options::Cheat::State::DISABLED;
return "Jail closed";
}
return "usage: cheat [infinite lives|invincibility|open the jail|close the jail]";
},
.hidden = CHEAT_HIDDEN,
.completions = {
{"CHEAT", {"INFINITE", "INVINCIBILITY", "OPEN", "CLOSE"}},
{"CHEAT INFINITE", {"LIVES"}},
{"CHEAT INFINITE LIVES", {"ON", "OFF"}},
{"CHEAT INVINCIBILITY", {"ON", "OFF"}},
{"CHEAT OPEN", {"THE"}},
{"CHEAT OPEN THE", {"JAIL"}},
{"CHEAT CLOSE", {"THE"}},
{"CHEAT CLOSE THE", {"JAIL"}},
}},
// SET PLAYER SKIN <1|2> — Cambia la skin del jugador (disponible en todos los builds, GAME)
// SET INITIAL [ROOM|POS] — Guarda habitación/posición actual como inicio (solo _DEBUG, GAME)
// SET INITIAL SCENE [<name>] — Guarda escena como escena inicial de debug (solo _DEBUG)
// SET ITEMS <0-200> — Fija el contador de items recogidos (solo _DEBUG, GAME)
{.keyword = "SET", .execute = [](const std::vector<std::string>& args) -> std::string {
if (args.size() >= 3 && args[0] == "PLAYER" && args[1] == "SKIN") {
if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
int num = 0;
try {
num = std::stoi(args[2]);
} catch (...) {}
if (num < 1 || num > 2) { return "usage: set player skin <1|2>"; }
if (!GameControl::change_player_skin) { return "Game not initialized"; }
GameControl::change_player_skin(num);
return "Player skin: " + std::to_string(num);
}
#ifdef _DEBUG
// SET INITIAL SCENE [<nombre>] — disponible desde cualquier escena
if (args.size() >= 2 && args[0] == "INITIAL" && args[1] == "SCENE") {
SceneManager::Scene target = SceneManager::current;
std::string name = "current";
if (args.size() >= 3) {
if (args[2] == "GAME") {
target = SceneManager::Scene::GAME;
name = "game";
} else if (args[2] == "LOGO") {
target = SceneManager::Scene::LOGO;
name = "logo";
} else if (args[2] == "LOADING") {
target = SceneManager::Scene::LOADING_SCREEN;
name = "loading";
} else if (args[2] == "TITLE") {
target = SceneManager::Scene::TITLE;
name = "title";
} else if (args[2] == "CREDITS") {
target = SceneManager::Scene::CREDITS;
name = "credits";
} else if (args[2] == "ENDING") {
target = SceneManager::Scene::ENDING;
name = "ending";
} else if (args[2] == "ENDING2") {
target = SceneManager::Scene::ENDING2;
name = "ending2";
} else {
std::string scene_lower = args[2];
std::ranges::transform(scene_lower, scene_lower.begin(), ::tolower);
return "Unknown scene: " + scene_lower;
}
}
Debug::get()->setInitialScene(target);
Debug::get()->saveToFile();
return "Initial scene set to: " + name;
}
if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
// SET ITEMS <0-200> — Fija el contador de items recogidos
if (args[0] == "ITEMS") {
if (args.size() < 2) { return "usage: set items <0-200>"; }
int count = 0;
try {
count = std::stoi(args[1]);
} catch (...) { return "usage: set items <0-200>"; }
if (count < 0 || count > 200) { return "Items must be between 0 and 200"; }
if (!GameControl::set_items) { return "Game not initialized"; }
GameControl::set_items(count);
return "Items: " + std::to_string(count);
}
if (args.empty() || args[0] != "INITIAL") { return "usage: set initial [room|pos|scene] | set items <0-200> | set player skin <1|2>"; }
const bool DO_ROOM = args.size() == 1 || (args.size() >= 2 && args[1] == "ROOM");
const bool DO_POS = args.size() == 1 || (args.size() >= 2 && args[1] == "POS");
if (!DO_ROOM && !DO_POS) { return "usage: set initial [room|pos|scene]"; }
if (!GameControl::set_initial_room || !GameControl::set_initial_pos) { return "Game not initialized"; }
std::string result;
if (DO_ROOM) { result = GameControl::set_initial_room(); }
if (DO_POS) {
if (!result.empty()) { result += ", "; }
result += GameControl::set_initial_pos();
}
return result;
#else
return "usage: set player skin <1|2>";
#endif
},
.completions = {
#ifdef _DEBUG
{"SET", {"PLAYER", "INITIAL", "ITEMS"}},
{"SET PLAYER", {"SKIN"}},
{"SET INITIAL", {"ROOM", "POS", "SCENE"}},
{"SET INITIAL SCENE", {"LOGO", "LOADING", "TITLE", "CREDITS", "GAME", "ENDING", "ENDING2"}},
#else
{"SET", {"PLAYER"}},
{"SET PLAYER", {"SKIN"}},
#endif
}},
#ifdef _DEBUG
// SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART] — Cambiar o reiniciar escena; solo en Debug
{.keyword = "SCENE", .execute = [](const std::vector<std::string>& args) -> std::string {
if (Options::kiosk.enabled) { return "Not allowed in kiosk mode"; }
if (args.empty()) { return "usage: scene [logo|loading|title|credits|game|ending|ending2|restart]"; }
// RESTART: reinicia la escena actual (funciona desde cualquier escena)
if (args[0] == "RESTART") {
SceneManager::scene_before_restart = SceneManager::current;
SceneManager::current = SceneManager::Scene::RESTART_CURRENT;
return "Restarting...";
}
// Para el resto: si pedimos la escena que ya está activa → también reiniciar
const auto GO_TO = [](SceneManager::Scene target, const std::string& label) -> std::string {
if (SceneManager::current == target) {
SceneManager::scene_before_restart = target;
SceneManager::current = SceneManager::Scene::RESTART_CURRENT;
} else {
SceneManager::current = target;
}
return "Scene: " + label;
};
if (args[0] == "LOGO") { return GO_TO(SceneManager::Scene::LOGO, "Logo"); }
if (args[0] == "LOADING") { return GO_TO(SceneManager::Scene::LOADING_SCREEN, "Loading"); }
if (args[0] == "TITLE") { return GO_TO(SceneManager::Scene::TITLE, "Title"); }
if (args[0] == "CREDITS") { return GO_TO(SceneManager::Scene::CREDITS, "Credits"); }
if (args[0] == "GAME") { return GO_TO(SceneManager::Scene::GAME, "Game"); }
if (args[0] == "ENDING") { return GO_TO(SceneManager::Scene::ENDING, "Ending"); }
if (args[0] == "ENDING2") { return GO_TO(SceneManager::Scene::ENDING2, "Ending 2"); }
return "Unknown scene: " + args[0];
},
.completions = {{"SCENE", {"LOGO", "LOADING", "TITLE", "CREDITS", "GAME", "ENDING", "ENDING2", "RESTART"}}}},
#endif
// RESTART — Reiniciar desde el principio (equivale a SCENE LOGO)
{.keyword = "RESTART", .execute = [](const std::vector<std::string>&) -> std::string {
SceneManager::current = SceneManager::Scene::LOGO;
Audio::get()->stopMusic();
return "Restarting...";
},
.instant = true},
// KIOSK [ON|OFF PLEASE|PLEASE] — Modo kiosko
{.keyword = "KIOSK", .execute = [](const std::vector<std::string>& args) -> std::string {
const bool DISABLE = (!args.empty() && args[0] == "PLEASE") ||
(args.size() >= 2 && args[0] == "OFF" && args[1] == "PLEASE");
if (DISABLE) {
Options::kiosk.enabled = false;
return "Kiosk mode OFF";
}
if (!args.empty() && args[0] == "OFF") {
return "Not allowed in kiosk mode";
}
if (args.empty() || args[0] == "ON") {
if (Options::kiosk.enabled) { return "Kiosk mode already ON"; }
Options::kiosk.enabled = true;
if (!Options::video.fullscreen) { Screen::get()->toggleVideoMode(); }
return "Kiosk mode ON";
}
return "usage: kiosk [on]";
},
.completions = {{"KIOSK", {"ON"}}}},
// AUDIO [ON|OFF|VOL <0-100>] — Audio maestro (estado + volumen)
{.keyword = "AUDIO", .execute = [](const std::vector<std::string>& args) -> std::string {
if (args.empty()) {
const int VOL = static_cast<int>(Options::audio.volume * 100.0F);
return std::string("Audio ") + (Options::audio.enabled ? "ON" : "OFF") +
" vol:" + std::to_string(VOL);
}
if (args[0] == "ON") {
if (Options::audio.enabled) { return "Audio already ON"; }
Options::audio.enabled = true;
Audio::get()->enable(true);
return "Audio ON";
}
if (args[0] == "OFF") {
if (!Options::audio.enabled) { return "Audio already OFF"; }
Options::audio.enabled = false;
Audio::get()->enable(false);
return "Audio OFF";
}
if (args[0] == "VOL" && args.size() >= 2) {
try {
const int VAL = std::stoi(args[1]);
if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; }
Options::audio.volume = static_cast<float>(VAL) / 100.0F;
Audio::get()->enable(Options::audio.enabled);
return "Audio vol:" + std::to_string(VAL);
} catch (...) { return "usage: audio vol <0-100>"; }
}
return "usage: audio [on|off|vol n]";
},
.completions = {{"AUDIO", {"ON", "OFF", "VOL"}}}},
// MUSIC [ON|OFF|VOL <0-100>] — Volumen e interruptor de música
{.keyword = "MUSIC", .execute = [](const std::vector<std::string>& args) -> std::string {
if (args.empty()) {
const int VOL = static_cast<int>(Options::audio.music.volume * 100.0F);
return std::string("Music ") + (Options::audio.music.enabled ? "ON" : "OFF") +
" vol:" + std::to_string(VOL);
}
if (args[0] == "ON") {
if (Options::audio.music.enabled) { return "Music already ON"; }
Options::audio.music.enabled = true;
Audio::get()->enableMusic(true);
Audio::get()->setMusicVolume(Options::audio.music.volume);
return "Music ON";
}
if (args[0] == "OFF") {
if (!Options::audio.music.enabled) { return "Music already OFF"; }
Audio::get()->setMusicVolume(0.0F);
Audio::get()->enableMusic(false);
Options::audio.music.enabled = false;
return "Music OFF";
}
if (args[0] == "VOL" && args.size() >= 2) {
try {
const int VAL = std::stoi(args[1]);
if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; }
Options::audio.music.volume = static_cast<float>(VAL) / 100.0F;
if (Options::audio.music.enabled) {
Audio::get()->setMusicVolume(Options::audio.music.volume);
}
return "Music vol:" + std::to_string(VAL);
} catch (...) { return "usage: music vol <0-100>"; }
}
return "usage: music [on|off|vol n]";
},
.completions = {{"MUSIC", {"ON", "OFF", "VOL"}}}},
// SOUND [ON|OFF|VOL <0-100>] — Volumen e interruptor de efectos de sonido
{.keyword = "SOUND", .execute = [](const std::vector<std::string>& args) -> std::string {
if (args.empty()) {
const int VOL = static_cast<int>(Options::audio.sound.volume * 100.0F);
return std::string("Sound ") + (Options::audio.sound.enabled ? "ON" : "OFF") +
" vol:" + std::to_string(VOL);
}
if (args[0] == "ON") {
if (Options::audio.sound.enabled) { return "Sound already ON"; }
Options::audio.sound.enabled = true;
Audio::get()->enableSound(true);
Audio::get()->setSoundVolume(Options::audio.sound.volume);
return "Sound ON";
}
if (args[0] == "OFF") {
if (!Options::audio.sound.enabled) { return "Sound already OFF"; }
Audio::get()->setSoundVolume(0.0F);
Audio::get()->enableSound(false);
Options::audio.sound.enabled = false;
return "Sound OFF";
}
if (args[0] == "VOL" && args.size() >= 2) {
try {
const int VAL = std::stoi(args[1]);
if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; }
Options::audio.sound.volume = static_cast<float>(VAL) / 100.0F;
if (Options::audio.sound.enabled) {
Audio::get()->setSoundVolume(Options::audio.sound.volume);
}
return "Sound vol:" + std::to_string(VAL);
} catch (...) { return "usage: sound vol <0-100>"; }
}
return "usage: sound [on|off|vol n]";
},
.completions = {{"SOUND", {"ON", "OFF", "VOL"}}}},
// EXIT / QUIT — Cerrar la aplicacion (bloqueado en kiosk)
{.keyword = "EXIT", .execute = [](const std::vector<std::string>& args) -> std::string {
if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) {
return "Not allowed in kiosk mode";
}
SceneManager::current = SceneManager::Scene::QUIT;
return "Quitting...";
},
.instant = true},
{.keyword = "QUIT", .execute = [](const std::vector<std::string>& args) -> std::string {
if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) {
return "Not allowed in kiosk mode";
}
SceneManager::current = SceneManager::Scene::QUIT;
return "Quitting...";
},
.instant = true},
// SIZE — Devuelve el tamaño actual de la ventana en píxeles
{.keyword = "SIZE", .execute = [](const std::vector<std::string>&) -> std::string {
int w = 0;
int h = 0;
SDL_GetWindowSize(SDL_GetRenderWindow(Screen::get()->getRenderer()), &w, &h);
return std::to_string(w) + "x" + std::to_string(h);
}},
// HELP / ? — Muestra ayuda en la terminal del sistema y lista de comandos en consola
{.keyword = "HELP", .execute = [](const std::vector<std::string>&) -> std::string {
printHelp();
std::string result =
"Commands:\n"
"fullscreen, zoom, intscale, vsync, driver, palette, audio, music, sound, set, restart, kiosk, exit, quit, show, hide, size, help\n";
#ifdef _DEBUG
result +=
"\nDebug commands:\n"
"debug, room, scene, cheat\n";
#endif
result += "-- more info on the terminal";
return result;
}},
{.keyword = "?", .execute = [](const std::vector<std::string>&) -> std::string {
printHelp();
std::string result =
"Commands:\n"
"fullscreen, zoom, intscale, vsync, driver, palette, audio, music, sound, set, restart, kiosk, exit, quit, show, hide, size, help\n";
#ifdef _DEBUG
result +=
"\nDebug commands:\n"
"debug, room, scene, cheat\n";
#endif
result += "-- more info on the terminal";
return result;
}},
};
// ── Helpers ─────────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────────
// Calcula la altura total de la consola para N líneas de mensaje (+ 1 línea de input) // Calcula la altura total de la consola para N líneas de mensaje (+ 1 línea de input)
@@ -979,13 +109,8 @@ Console::Console(const std::string& font_name)
target_height_ = height_; target_height_ = height_;
y_ = -height_; y_ = -height_;
// Construir mapa de autocompletado a partir de COMMANDS // Cargar comandos desde YAML
for (const auto& cmd : COMMANDS) { registry_.load("data/console/commands.yaml");
for (const auto& [path, opts] : cmd.completions) {
auto& vec = tab_completions_[std::string(path)];
for (const auto& opt : opts) { vec.emplace_back(opt); }
}
}
buildSurface(); buildSurface();
} }
@@ -1032,7 +157,7 @@ void Console::redrawText() {
// Línea de input (siempre la última) // Línea de input (siempre la última)
const bool SHOW_CURSOR = cursor_visible_ && (static_cast<int>(input_line_.size()) < MAX_LINE_CHARS); const bool SHOW_CURSOR = cursor_visible_ && (static_cast<int>(input_line_.size()) < MAX_LINE_CHARS);
const std::string INPUT_STR = "> " + input_line_ + (SHOW_CURSOR ? "_" : ""); const std::string INPUT_STR = prompt_ + input_line_ + (SHOW_CURSOR ? "_" : "");
text_->writeColored(PADDING_IN_H, y_pos, INPUT_STR, BORDER_COLOR); text_->writeColored(PADDING_IN_H, y_pos, INPUT_STR, BORDER_COLOR);
Screen::get()->setRendererSurface(previous_renderer); Screen::get()->setRendererSurface(previous_renderer);
@@ -1225,36 +350,19 @@ void Console::handleEvent(const SDL_Event& event) {
const size_t space_pos = upper.rfind(' '); const size_t space_pos = upper.rfind(' ');
if (space_pos == std::string::npos) { if (space_pos == std::string::npos) {
// Modo comando: ciclar keywords que empiecen por el prefijo // Modo comando: ciclar keywords visibles que empiecen por el prefijo
for (const auto& cmd : COMMANDS) { for (const auto& kw : registry_.getVisibleKeywords()) {
if (cmd.hidden) { continue; } if (upper.empty() || kw.starts_with(upper)) {
if (upper.empty() || cmd.keyword.starts_with(upper)) { tab_matches_.emplace_back(kw);
tab_matches_.emplace_back(cmd.keyword);
} }
} }
} else { } else {
const std::string base_cmd = upper.substr(0, space_pos); const std::string base_cmd = upper.substr(0, space_pos);
const std::string sub_prefix = upper.substr(space_pos + 1); const std::string sub_prefix = upper.substr(space_pos + 1);
if (base_cmd == "PALETTE" && Screen::get() != nullptr) { const auto opts = registry_.getCompletions(base_cmd);
// NEXT/PREV primero, luego todos los nombres de paleta disponibles for (const auto& arg : opts) {
for (const auto* sv : {"NEXT", "PREV"}) { if (sub_prefix.empty() || std::string_view{arg}.starts_with(sub_prefix)) {
if (sub_prefix.empty() || std::string_view{sv}.starts_with(sub_prefix)) { tab_matches_.emplace_back(base_cmd + " " + arg);
tab_matches_.emplace_back("PALETTE " + std::string(sv));
}
}
for (const auto& name : Screen::get()->getPaletteNames()) {
if (sub_prefix.empty() || std::string_view{name}.starts_with(sub_prefix)) {
tab_matches_.emplace_back("PALETTE " + name);
}
}
} else {
const auto it = tab_completions_.find(base_cmd);
if (it != tab_completions_.end()) {
for (const auto& arg : it->second) {
if (sub_prefix.empty() || std::string_view{arg}.starts_with(sub_prefix)) {
tab_matches_.emplace_back(base_cmd + " " + arg);
}
}
} }
} }
} }
@@ -1289,17 +397,13 @@ void Console::processCommand() {
const std::string& cmd = TOKENS[0]; const std::string& cmd = TOKENS[0];
const std::vector<std::string> ARGS(TOKENS.begin() + 1, TOKENS.end()); const std::vector<std::string> ARGS(TOKENS.begin() + 1, TOKENS.end());
std::string result; std::string result;
bool found = false;
bool instant = false; bool instant = false;
for (const auto& command : COMMANDS) {
if (command.keyword == cmd) { const auto* def = registry_.findCommand(cmd);
result = command.execute(ARGS); if (def != nullptr) {
instant = command.instant; result = registry_.execute(cmd, ARGS);
found = true; instant = def->instant;
break; } else {
}
}
if (!found) {
std::string cmd_lower = cmd; std::string cmd_lower = cmd;
std::ranges::transform(cmd_lower, cmd_lower.begin(), ::tolower); std::ranges::transform(cmd_lower, cmd_lower.begin(), ::tolower);
result = "Unknown: " + cmd_lower; result = "Unknown: " + cmd_lower;

View File

@@ -2,12 +2,13 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <deque> // Para deque (historial) #include <deque> // Para deque (historial)
#include <functional> // Para function #include <functional> // Para function
#include <memory> // Para shared_ptr #include <memory> // Para shared_ptr
#include <string> // Para string #include <string> // Para string
#include <unordered_map> // Para unordered_map (tab_completions_) #include <vector> // Para vector
#include <vector> // Para vector
#include "game/ui/console_commands.hpp" // Para CommandRegistry
class Surface; class Surface;
class Sprite; class Sprite;
@@ -31,6 +32,9 @@ class Console {
auto getVisibleHeight() -> int; // Píxeles visibles actuales (0 = oculta, height_ = totalmente visible) auto getVisibleHeight() -> int; // Píxeles visibles actuales (0 = oculta, height_ = totalmente visible)
[[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; } [[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; }
// Prompt configurable (por defecto "> ")
void setPrompt(const std::string& prompt) { prompt_ = prompt; }
// Callback llamado al abrir (true) o cerrar (false) la consola // Callback llamado al abrir (true) o cerrar (false) la consola
std::function<void(bool)> on_toggle; std::function<void(bool)> on_toggle;
@@ -83,6 +87,7 @@ class Console {
// Estado de la entrada de texto // Estado de la entrada de texto
std::vector<std::string> msg_lines_; // Líneas de mensaje (1 o más) std::vector<std::string> msg_lines_; // Líneas de mensaje (1 o más)
std::string input_line_; std::string input_line_;
std::string prompt_{"> "}; // Prompt configurable
float cursor_timer_{0.0F}; float cursor_timer_{0.0F};
bool cursor_visible_{true}; bool cursor_visible_{true};
@@ -100,7 +105,9 @@ class Console {
std::string saved_input_; // guarda input_line_ al empezar a navegar std::string saved_input_; // guarda input_line_ al empezar a navegar
// Estado de autocompletado (TAB) // Estado de autocompletado (TAB)
std::vector<std::string> tab_matches_; // Comandos que coinciden con el prefijo actual std::vector<std::string> tab_matches_; // Comandos que coinciden con el prefijo actual
int tab_index_{-1}; // Índice actual en tab_matches_ int tab_index_{-1}; // Índice actual en tab_matches_
std::unordered_map<std::string, std::vector<std::string>> tab_completions_; // Mapa pre-calculado en constructor
// Registro de comandos (metadatos YAML + handlers C++)
CommandRegistry registry_;
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
#pragma once
#include <functional> // Para function
#include <string> // Para string
#include <unordered_map> // Para unordered_map
#include <vector> // Para vector
// Definición de un comando de consola (metadatos cargados desde YAML)
struct CommandDef {
std::string keyword;
std::string handler_id;
std::string category;
std::string description;
std::string usage;
bool instant{false};
bool hidden{false};
bool debug_only{false};
bool help_hidden{false};
bool dynamic_completions{false};
std::unordered_map<std::string, std::vector<std::string>> completions;
};
// Tipo de función handler para comandos
using CommandHandler = std::function<std::string(const std::vector<std::string>& args)>;
// Proveedor de completions dinámicas: devuelve las opciones para TAB en UPPERCASE
using DynamicCompletionProvider = std::function<std::vector<std::string>()>;
// Registro de comandos: une metadatos YAML con handlers C++
class CommandRegistry {
public:
// Carga los metadatos de comandos desde un archivo YAML y registra los handlers
void load(const std::string& yaml_path);
// Búsqueda y ejecución
[[nodiscard]] auto findCommand(const std::string& keyword) const -> const CommandDef*;
auto execute(const std::string& keyword, const std::vector<std::string>& args) const -> std::string;
// Generación de ayuda (auto-generada desde los metadatos)
[[nodiscard]] auto generateTerminalHelp() const -> std::string;
[[nodiscard]] auto generateConsoleHelp() const -> std::string;
// TAB completion
// Devuelve las opciones de completado para un path dado (ej: "SHADER", "SHADER PRESET")
// Combina completions estáticas del YAML con dinámicas registradas en C++
[[nodiscard]] auto getCompletions(const std::string& path) const -> std::vector<std::string>;
[[nodiscard]] auto getVisibleKeywords() const -> std::vector<std::string>;
private:
std::vector<CommandDef> commands_;
std::unordered_map<std::string, CommandHandler> handlers_;
std::unordered_map<std::string, std::vector<std::string>> completions_map_;
std::unordered_map<std::string, DynamicCompletionProvider> dynamic_providers_;
void registerHandlers();
};

View File

@@ -6,7 +6,7 @@
namespace Texts { namespace Texts {
constexpr const char* WINDOW_CAPTION = "© 2022 JailDoctor's Dilemma — JailDesigner"; constexpr const char* WINDOW_CAPTION = "© 2022 JailDoctor's Dilemma — JailDesigner";
constexpr const char* COPYRIGHT = "@2022 JailDesigner"; constexpr const char* COPYRIGHT = "@2022 JailDesigner";
constexpr const char* VERSION = "1.11"; // Versión por defecto constexpr const char* VERSION = "1.13"; // Versión por defecto
} // namespace Texts } // namespace Texts
// Tamaño de bloque // Tamaño de bloque

View File

@@ -414,6 +414,13 @@ auto toUpper(const std::string& str) -> std::string {
return upper_str; return upper_str;
} }
// Convierte guiones a espacios ("crt-live" → "crt live")
auto prettyName(const std::string& str) -> std::string {
std::string result = str;
std::ranges::replace(result, '-', ' ');
return result;
}
// Obtiene el nombre de un fichero a partir de una ruta completa // Obtiene el nombre de un fichero a partir de una ruta completa
auto getFileName(const std::string& path) -> std::string { auto getFileName(const std::string& path) -> std::string {
return std::filesystem::path(path).filename().string(); return std::filesystem::path(path).filename().string();

View File

@@ -98,6 +98,7 @@ auto stringToBool(const std::string& str) -> bool; // Strin
auto boolToString(bool value) -> std::string; // Bool a string (1/0) auto boolToString(bool value) -> std::string; // Bool a string (1/0)
auto toLower(const std::string& str) -> std::string; // String a minúsculas auto toLower(const std::string& str) -> std::string; // String a minúsculas
auto toUpper(const std::string& str) -> std::string; // String a mayúsculas auto toUpper(const std::string& str) -> std::string; // String a mayúsculas
auto prettyName(const std::string& str) -> std::string; // Guiones a espacios ("crt-live" → "crt live")
// OPERACIONES CON STRINGS // OPERACIONES CON STRINGS
auto stringInVector(const std::vector<std::string>& vec, const std::string& str) -> bool; // Busca string en vector auto stringInVector(const std::vector<std::string>& vec, const std::string& str) -> bool; // Busca string en vector

View File

@@ -0,0 +1,125 @@
import sys
import math
import tempfile
import os
# ------------------------------
# Utilidades de color
# ------------------------------
def luminosidad(rgb):
r, g, b = rgb
return 0.2126*r + 0.7152*g + 0.0722*b
def distancia_rgb(c1, c2):
return math.sqrt(
(c1[0] - c2[0])**2 +
(c1[1] - c2[1])**2 +
(c1[2] - c2[2])**2
)
# Paleta ZX Spectrum
PALETA_SPECTRUM = [
(0, 0, 0),
(0, 0, 0),
(0, 0, 216),
(0, 0, 255),
(216, 0, 0),
(255, 0, 0),
(216, 0, 216),
(255, 0, 255),
(0, 216, 0),
(0, 255, 0),
(0, 216, 216),
(0, 255, 255),
(216, 216, 0),
(255, 255, 0),
(216, 216, 216),
(255, 255, 255),
]
# ------------------------------
# Lectura / escritura JASC-PAL
# ------------------------------
def leer_paleta_jasc(ruta):
with open(ruta, "r") as f:
lineas = [l.strip() for l in f.readlines()]
if lineas[0] != "JASC-PAL":
raise ValueError("El fichero no es un JASC-PAL válido")
num_colores = int(lineas[2])
colores = []
for i in range(num_colores):
r, g, b = map(int, lineas[3 + i].split())
colores.append((r, g, b))
return colores
def guardar_paleta_jasc(ruta, colores):
with open(ruta, "w") as f:
f.write("JASC-PAL\n")
f.write("0100\n")
f.write(f"{len(colores)}\n")
for r, g, b in colores:
f.write(f"{r} {g} {b}\n")
# ------------------------------
# Métodos de ordenación
# ------------------------------
def ordenar_por_luminosidad(colores):
return sorted(colores, key=luminosidad)
def ordenar_por_similitud_spectrum(colores):
colores_disponibles = colores.copy()
resultado = []
for ref in PALETA_SPECTRUM:
mejor = min(colores_disponibles, key=lambda c: distancia_rgb(c, ref))
resultado.append(mejor)
colores_disponibles.remove(mejor)
return resultado
# ------------------------------
# Main
# ------------------------------
def main():
if len(sys.argv) < 4:
print("Uso: python ordenar_paleta.py entrada.pal salida.pal [luminosidad|spectrum]")
return
entrada = sys.argv[1]
salida = sys.argv[2]
modo = sys.argv[3].lower()
colores = leer_paleta_jasc(entrada)
if modo == "luminosidad":
colores_ordenados = ordenar_por_luminosidad(colores)
elif modo == "spectrum":
colores_ordenados = ordenar_por_similitud_spectrum(colores)
else:
print(f"Modo desconocido: {modo}")
print("Modos disponibles: luminosidad, spectrum")
return
# Si salida == entrada, sobrescribimos de forma segura
if entrada == salida:
# Guardamos primero en un temporal para evitar corrupción si algo falla
with tempfile.NamedTemporaryFile(delete=False) as tmp:
temp_path = tmp.name
guardar_paleta_jasc(temp_path, colores_ordenados)
os.replace(temp_path, entrada)
print(f"Paleta sobrescrita ({modo}) en {entrada}")
else:
guardar_paleta_jasc(salida, colores_ordenados)
print(f"Paleta ordenada ({modo}) guardada en {salida}")
if __name__ == "__main__":
main()