12 Commits
v1.11 ... main

Author SHA1 Message Date
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
56 changed files with 2356 additions and 1325 deletions

2
.gitignore vendored
View File

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

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

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

143
CHANGELOG.md Normal file
View File

@@ -0,0 +1,143 @@
# Changelog
All notable changes to JailDoctor's Dilemma are documented here.
---
## [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/).*

View File

@@ -98,6 +98,7 @@ set(APP_SOURCES
# Game - UI
source/game/ui/console.cpp
source/game/ui/console_commands.cpp
source/game/ui/notifier.cpp
# Utils

View File

@@ -32,7 +32,7 @@ assets:
- type: PALETTE
path: ${PREFIX}/data/palette/zx-spectrum-adjusted.pal
- type: PALETTE
path: ${PREFIX}/data/palette/zxarne-5-2.pal
path: ${PREFIX}/data/palette/zxarne-5.2.pal
- type: PALETTE
path: ${PREFIX}/data/palette/black-and-white.pal
- type: PALETTE
@@ -46,15 +46,33 @@ assets:
- type: PALETTE
path: ${PREFIX}/data/palette/pico-8.pal
- type: PALETTE
path: ${PREFIX}/data/palette/sweetie-16.pal
path: ${PREFIX}/data/palette/sweetie.pal
- type: PALETTE
path: ${PREFIX}/data/palette/island-joy-16.pal
path: ${PREFIX}/data/palette/island-joy.pal
- type: PALETTE
path: ${PREFIX}/data/palette/lost-century.pal
- type: PALETTE
path: ${PREFIX}/data/palette/na16.pal
path: ${PREFIX}/data/palette/na.pal
- type: PALETTE
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:
@@ -99,6 +117,11 @@ assets:
required: false
absolute: true
# CONSOLE
console:
- type: DATA
path: ${PREFIX}/data/console/commands.yaml
# ROOMS
rooms:
- type: ROOM

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

@@ -0,0 +1,230 @@
# 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|<name>]"
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"
completions:
ROOM: [NEXT, PREV]
- 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]
- 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

@@ -125,6 +125,8 @@ scoreboard:
items: "TRESORS PILLATS "
time: " HORA "
rooms: "SALES"
cheat_infinite_lives: "vides inf"
cheat_invincibility: "inv"
game:
music_enabled: "MÚSICA ACTIVADA"

View File

@@ -125,6 +125,8 @@ scoreboard:
items: "ITEMS COLLECTED "
time: " TIME "
rooms: "ROOMS"
cheat_infinite_lives: "inf lives"
cheat_invincibility: "inv"
game:
music_enabled: "MUSIC ENABLED"

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
67 0 103
22 23 26
127 6 34
0 40 89
148 33 106
35 73 117
214 36 17
255 38 116
0 120 153
255 132 38
255 128 164
104 174 212
16 210 117
255 209 0
191 255 60
250 253 255

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
31 32 37
46 51 77
76 82 116
124 143 178
128 77 83
191 125 133
114 51 76
192 165 169
82 103 93
108 154 154
108 154 154
226 217 228
243 200 147
229 152 125
192 165 169
226 217 228

View File

@@ -1,19 +1,19 @@
JASC-PAL
0100
16
255 255 255
109 247 193
17 173 193
96 108 129
57 52 87
30 136 117
91 179 97
161 229 90
247 228 118
249 146 82
203 77 104
106 55 113
201 36 100
203 77 104
96 108 129
30 136 117
17 173 193
155 156 130
91 179 97
249 146 82
244 140 182
247 182 158
155 156 130
161 229 90
109 247 193
247 228 118
255 255 255

View File

@@ -1,19 +1,19 @@
JASC-PAL
0100
16
209 177 135
199 123 88
174 93 64
121 68 74
75 61 68
186 145 88
146 116 65
77 69 57
119 116 59
179 165 85
210 201 165
140 171 161
75 114 110
87 72 82
121 68 74
75 114 110
174 93 64
119 116 59
146 116 65
132 120 117
199 123 88
186 145 88
171 155 142
179 165 85
140 171 161
209 177 135
210 201 165

View File

@@ -1,19 +1,19 @@
JASC-PAL
0100
16
140 143 174
88 69 99
62 33 55
154 99 72
215 155 125
245 237 186
192 199 65
100 125 52
228 148 58
157 48 59
210 100 113
112 55 127
126 196 193
52 133 157
23 67 75
31 14 28
62 33 55
23 67 75
157 48 59
112 55 127
88 69 99
154 99 72
100 125 52
52 133 157
210 100 113
140 143 174
228 148 58
215 155 125
126 196 193
192 199 65
245 237 186

View File

@@ -3,17 +3,17 @@ JASC-PAL
16
0 0 0
29 43 83
126 37 83
0 135 81
171 82 54
95 87 79
194 195 199
255 241 232
255 0 77
255 163 0
255 236 39
0 228 54
41 173 255
131 118 156
255 0 77
171 82 54
255 119 168
194 195 199
0 228 54
0 135 81
95 87 79
255 241 232
255 236 39
255 163 0
255 204 170
126 37 83

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

@@ -2,18 +2,18 @@ JASC-PAL
0100
16
17 17 37
82 75 109
176 201 196
255 252 241
36 34 114
52 112 190
159 32 98
255 94 57
150 58 191
255 94 57
159 32 98
255 105 246
176 201 196
44 126 75
160 195 95
67 152 196
147 255 229
210 133 55
254 245 107
255 252 241
82 75 109

View File

@@ -2,18 +2,18 @@ JASC-PAL
0100
16
15 11 56
97 106 130
173 180 183
249 255 236
122 87 22
40 19 160
74 107 255
160 35 17
237 23 95
115 16 147
238 20 181
115 16 147
39 139 97
157 255 38
27 105 167
71 233 223
122 87 22
27 105 167
247 229 77
173 180 183
249 255 236
97 106 130

View File

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

View File

@@ -1,19 +1,19 @@
JASC-PAL
0100
16
33 59 37
58 96 74
79 119 84
161 159 124
119 116 79
119 92 79
96 59 58
59 33 55
23 14 25
47 33 59
59 33 55
33 59 37
67 58 96
96 59 58
79 82 119
58 96 74
119 92 79
79 119 84
101 115 140
119 116 79
124 148 161
161 159 124
160 185 186
192 209 204

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
16
26 28 44
41 54 111
51 60 87
86 108 134
59 93 201
37 113 121
93 39 93
41 54 111
177 62 83
239 125 87
93 39 93
148 176 194
56 183 100
167 240 112
37 113 121
65 166 246
115 239 247
239 125 87
255 205 117
148 176 194
167 240 112
244 244 244
86 108 134

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
50 50 171
35 103 239
254 1 69
119 70 2
118 50 118
239 152 152
1 137 84
1 186 152
152 186 220
253 253 253
254 239 69
186 118 84
254 205 205
84 1 103

View File

@@ -4,17 +4,7 @@ frameWidth: 8
frameHeight: 16
animations:
- name: stand
speed: 0.1333
loop: 0
frames: [0]
- name: walk
- name: default
speed: 0.1333
loop: 0
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
animations:
- name: stand
speed: 0.1333
loop: 0
frames: [0]
- name: walk
- name: default
speed: 0.1333
loop: 0
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() {
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() {
if (Options::current_shader == Rendering::ShaderType::CRTPI) {
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
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();
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 {
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();
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,17 @@ namespace GlobalInputs {
void handleNextShader() {
Screen::get()->nextShader();
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() {
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() {
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 handleToggleIntegerScale() {
@@ -160,14 +160,16 @@ namespace GlobalInputs {
return InputAction::WINDOW_INC_ZOOM;
}
}
if (Input::get()->checkAction(InputAction::TOGGLE_SHADER, Input::DO_NOT_ALLOW_REPEAT)) {
if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) {
return InputAction::TOGGLE_SUPERSAMPLING; // Ctrl+F4
if (Screen::get()->isHardwareAccelerated()) {
if (Input::get()->checkAction(InputAction::TOGGLE_SHADER, Input::DO_NOT_ALLOW_REPEAT)) {
if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) {
return InputAction::TOGGLE_SUPERSAMPLING; // Ctrl+F4
}
if (Options::video.shader.enabled && ((SDL_GetModState() & SDL_KMOD_SHIFT) != 0U)) {
return InputAction::NEXT_SHADER_PRESET; // Shift+F4
}
return InputAction::TOGGLE_SHADER; // F4
}
if (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)) {
return InputAction::NEXT_PALETTE;

View File

@@ -6,6 +6,7 @@
#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/defaults.hpp"
#include "game/options.hpp"
#include "utils/utils.hpp"
@@ -44,9 +45,9 @@ void PaletteManager::previous() {
}
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) {
if (toUpper(palettes_[i]) == upper_name) {
if (toLower(palettes_[i]) == lower_name) {
current_ = i;
apply();
return true;
@@ -62,7 +63,7 @@ auto PaletteManager::getNames() const -> std::vector<std::string> {
std::string name = p;
const size_t pos = name.find(".pal");
if (pos != std::string::npos) { name.erase(pos, 4); }
std::ranges::transform(name, name.begin(), ::toupper);
std::ranges::transform(name, name.begin(), ::tolower);
names.push_back(std::move(name));
}
return names;
@@ -72,7 +73,13 @@ auto PaletteManager::getCurrentName() const -> std::string {
std::string name = palettes_.at(current_);
const size_t pos = name.find(".pal");
if (pos != std::string::npos) { name.erase(pos, 4); }
std::ranges::transform(name, name.begin(), ::toupper);
std::ranges::transform(name, name.begin(), ::tolower);
return name;
}
auto PaletteManager::getPrettyName() const -> std::string {
std::string name = getCurrentName();
std::ranges::replace(name, '-', ' ');
return name;
}
@@ -88,9 +95,16 @@ void PaletteManager::apply() {
}
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) {
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;
}
}

View File

@@ -21,8 +21,9 @@ class PaletteManager {
void next(); // Avanza a la siguiente paleta
void previous(); // Retrocede a la paleta anterior
auto setByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe
[[nodiscard]] auto getNames() const -> std::vector<std::string>; // Nombres disponibles (mayúsculas, sin .pal)
[[nodiscard]] auto getCurrentName() const -> std::string; // Nombre de la paleta actual (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 (minúsculas, sin .pal)
[[nodiscard]] auto getPrettyName() const -> std::string; // Nombre actual con guiones sustituidos por espacios
private:
void apply(); // Aplica la paleta actual a ambas surfaces

View File

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

View File

@@ -239,11 +239,11 @@ void Screen::renderNotifications() const {
// Activa/desactiva todos los shaders respetando el shader actualmente seleccionado
void Screen::toggleShaders() {
Options::video.postfx = !Options::video.postfx;
Options::video.shader.enabled = !Options::video.shader.enabled;
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
if (Options::video.postfx) {
if (Options::video.shader.enabled) {
// 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);
applyCurrentCrtPiPreset();
} else {
@@ -262,10 +262,10 @@ void Screen::toggleShaders() {
// Recarga el shader del preset actual sin toggle
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
applyCurrentPostFXPreset();
} else if (Options::video.postfx) {
} else if (Options::video.shader.enabled) {
initShaders();
}
}
@@ -446,8 +446,9 @@ void Screen::renderOverlays() {
// Cambia a una paleta por nombre (case-insensitive); devuelve false si no existe
auto Screen::setPaletteByName(const std::string& name) -> bool { return palette_manager_->setByName(name); }
// Devuelve los nombres de paletas disponibles (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::getPalettePrettyName() const -> std::string { return palette_manager_->getPrettyName(); }
// Limpia la game_surface_
void Screen::clearSurface(Uint8 index) { game_surface_->clear(index); }
@@ -504,14 +505,14 @@ auto loadData(const std::string& filepath) -> std::vector<uint8_t> {
}
void Screen::setLinearUpscale(bool linear) {
Options::video.linear_upscale = linear;
Options::video.supersampling.linear_upscale = linear;
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
shader_backend_->setLinearUpscale(linear);
}
}
void Screen::setDownscaleAlgo(int algo) {
Options::video.downscale_algo = algo;
Options::video.supersampling.downscale_algo = algo;
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
shader_backend_->setDownscaleAlgo(algo);
}
@@ -524,8 +525,8 @@ auto Screen::getSsTextureSize() const -> std::pair<int, int> {
// Activa/desactiva el supersampling global (Ctrl+F4)
void Screen::toggleSupersampling() {
Options::video.supersampling = !Options::video.supersampling;
if (Options::video.postfx && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
Options::video.supersampling.enabled = !Options::video.supersampling.enabled;
if (Options::video.shader.enabled && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
applyCurrentPostFXPreset();
}
}
@@ -533,11 +534,11 @@ void Screen::toggleSupersampling() {
// Aplica los parámetros del preset actual al backend de shaders
void Screen::applyCurrentPostFXPreset() { // NOLINT(readability-convert-member-functions-to-static)
if (shader_backend_ && !Options::postfx_presets.empty()) {
const auto& p = Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)];
// Supersampling es un toggle global (Options::video.supersampling), no por 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.enabled), no por preset.
// setOversample primero: puede recrear texturas antes de que setPostFXParams
// decida si hornear scanlines en CPU o aplicarlas en GPU.
shader_backend_->setOversample(Options::video.supersampling ? 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};
shader_backend_->setPostFXParams(params);
}
@@ -546,7 +547,7 @@ void Screen::applyCurrentPostFXPreset() { // NOLINT(readability-convert-member-
// Aplica los parámetros del preset CrtPi actual al backend de shaders
void Screen::applyCurrentCrtPiPreset() { // NOLINT(readability-convert-member-functions-to-static)
if (shader_backend_ && !Options::crtpi_presets.empty()) {
const auto& p = Options::crtpi_presets[static_cast<size_t>(Options::current_crtpi_preset)];
const auto& p = Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)];
Rendering::CrtPiParams params{
.scanline_weight = p.scanline_weight,
.scanline_gap_brightness = p.scanline_gap_brightness,
@@ -569,9 +570,9 @@ void Screen::applyCurrentCrtPiPreset() { // NOLINT(readability-convert-member-f
// Cambia el shader de post-procesado activo y aplica el preset correspondiente
void Screen::setActiveShader(Rendering::ShaderType type) {
Options::current_shader = type;
Options::video.shader.current_shader = type;
if (!shader_backend_) { return; }
if (!Options::video.postfx) {
if (!Options::video.shader.enabled) {
// Shaders desactivados: guardar preferencia pero mantener pass-through
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
@@ -587,7 +588,7 @@ void Screen::setActiveShader(Rendering::ShaderType type) {
// Cicla al siguiente shader disponible (preparado para futura UI)
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::POSTFX;
setActiveShader(NEXT);
@@ -601,7 +602,8 @@ void Screen::initShaders() {
if (!shader_backend_) {
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, "", "");
gpu_driver_ = shader_backend_->getDriverName();
@@ -609,10 +611,10 @@ void Screen::initShaders() {
// Propagar flags de vsync, integer scale, upscale y downscale al backend GPU
shader_backend_->setVSync(Options::video.vertical_sync);
shader_backend_->setScaleMode(Options::video.integer_scale);
shader_backend_->setLinearUpscale(Options::video.linear_upscale);
shader_backend_->setDownscaleAlgo(Options::video.downscale_algo);
shader_backend_->setLinearUpscale(Options::video.supersampling.linear_upscale);
shader_backend_->setDownscaleAlgo(Options::video.supersampling.downscale_algo);
if (Options::video.postfx) {
if (Options::video.shader.enabled) {
applyCurrentPostFXPreset();
} else {
// Pass-through: todos los efectos a 0, el shader solo copia la textura
@@ -620,8 +622,8 @@ void Screen::initShaders() {
}
// Restaurar el shader activo guardado en config (y sus parámetros CrtPi si aplica)
shader_backend_->setActiveShader(Options::current_shader);
if (Options::current_shader == Rendering::ShaderType::CRTPI) {
shader_backend_->setActiveShader(Options::video.shader.current_shader);
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
applyCurrentCrtPiPreset();
}
}

View File

@@ -57,7 +57,8 @@ class Screen {
void nextPalette(); // Cambia a la siguiente paleta
void previousPalette(); // Cambia a la paleta anterior
auto setPaletteByName(const std::string& name) -> bool; // Cambia a paleta por nombre; false si no existe
[[nodiscard]] auto getPaletteNames() const -> std::vector<std::string>; // Nombres disponibles (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 toggleShaders(); // Activa/desactiva todos los shaders respetando current_shader
void toggleSupersampling(); // Activa/desactiva el supersampling global
void reloadPostFX(); // Recarga el shader del preset actual sin toggle
@@ -80,6 +81,7 @@ class Screen {
[[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; }
[[nodiscard]] auto getGameSurfaceDstRect() const -> SDL_FRect { return game_surface_dstrect_; }
[[nodiscard]] auto getGPUDriver() const -> const std::string& { return gpu_driver_; }
[[nodiscard]] auto isHardwareAccelerated() const -> bool { return shader_backend_ && shader_backend_->isHardwareAccelerated(); }
[[nodiscard]] auto getLastFPS() const -> int { return fps_.last_value; }
[[nodiscard]] auto getZoomFactor() const -> float { return zoom_factor_; }
[[nodiscard]] auto getMaxZoom() const -> int;

View File

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

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 y_end = std::min(rect->y + rect->h, surface_data_->height);
// Recorrer cada píxel dentro del rectángulo directamente
for (int y = y_start; y < y_end; ++y) {
for (int x = x_start; x < x_end; ++x) {
const int INDEX = x + (y * surface_data_->width);
surface_data_->data.get()[INDEX] = color;
}
// Rellenar fila a fila con memset (memoria contigua por fila)
Uint8* data_ptr = surface_data_->data.get();
const int surf_width = static_cast<int>(surface_data_->width);
const int row_width = static_cast<int>(x_end) - static_cast<int>(x_start);
for (int y = static_cast<int>(y_start); y < static_cast<int>(y_end); ++y) {
std::memset(data_ptr + (y * surf_width) + static_cast<int>(x_start), color, row_width);
}
}
@@ -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 y_end = std::min(rect->y + rect->h, surface_data_->height);
// Dibujar bordes horizontales
for (int x = x_start; x < x_end; ++x) {
// Borde superior
const int TOP_INDEX = x + (y_start * surface_data_->width);
surface_data_->data.get()[TOP_INDEX] = color;
// Borde inferior
const int BOTTOM_INDEX = x + ((y_end - 1) * surface_data_->width);
surface_data_->data.get()[BOTTOM_INDEX] = color;
}
// Dibujar bordes horizontales con memset (líneas contiguas en memoria)
Uint8* data_ptr = surface_data_->data.get();
const int surf_width = static_cast<int>(surface_data_->width);
const int row_width = static_cast<int>(x_end) - static_cast<int>(x_start);
std::memset(data_ptr + (static_cast<int>(y_start) * surf_width) + static_cast<int>(x_start), color, row_width);
std::memset(data_ptr + ((static_cast<int>(y_end) - 1) * surf_width) + static_cast<int>(x_start), color, row_width);
// Dibujar bordes verticales
for (int y = y_start; y < y_end; ++y) {
@@ -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);
h = std::min(h, surface_data->height - dy);
const Uint8* src_ptr = surface_data_->data.get();
Uint8* dst_ptr = surface_data->data.get();
for (int iy = 0; iy < h; ++iy) {
for (int ix = 0; ix < w; ++ix) {
// Verificar que las coordenadas de destino están dentro de los límites
@@ -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_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_)) {
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);
// Renderiza píxel por píxel aplicando el flip si es necesario
const Uint8* src_ptr = surface_data_->data.get();
Uint8* dst_ptr = surface_data_dest->data.get();
for (int iy = 0; iy < h; ++iy) {
for (int ix = 0; ix < w; ++ix) {
// Coordenadas de origen
@@ -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
if (dest_x >= 0 && dest_x < surface_data_dest->width && dest_y >= 0 && dest_y < surface_data_dest->height) {
// Copia el píxel si no es transparente
Uint8 color = 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_)) {
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)
int row_stride = pitch / sizeof(Uint32);
for (int y = 0; y < surface_data_->height; ++y) {
for (int x = 0; x < surface_data_->width; ++x) {
// Calcular la posición correcta en la textura teniendo en cuenta el stride
int texture_index = (y * row_stride) + x;
int surface_index = (y * surface_data_->width) + x;
pixels[texture_index] = palette_[surface_data_->data.get()[surface_index]];
// Cachear punteros fuera del bucle para permitir autovectorización SIMD
const Uint8* src = surface_data_->data.get();
const Uint32* pal = palette_.data();
const int width = surface_data_->width;
const int height = surface_data_->height;
for (int y = 0; y < height; ++y) {
const Uint8* src_row = src + (y * width);
Uint32* dst_row = pixels + (y * row_stride);
for (int x = 0; x < width; ++x) {
dst_row[x] = pal[src_row[x]];
}
}
@@ -613,12 +616,16 @@ void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FR
int row_stride = pitch / sizeof(Uint32);
for (int y = 0; y < surface_data_->height; ++y) {
for (int x = 0; x < surface_data_->width; ++x) {
int texture_index = (y * row_stride) + x;
int surface_index = (y * surface_data_->width) + x;
pixels[texture_index] = palette_[surface_data_->data.get()[surface_index]];
// Cachear punteros fuera del bucle para permitir autovectorización SIMD
const Uint8* src = surface_data_->data.get();
const Uint32* pal = palette_.data();
const int width = surface_data_->width;
const int height = surface_data_->height;
for (int y = 0; y < height; ++y) {
const Uint8* src_row = src + (y * width);
Uint32* dst_row = pixels + (y * row_stride);
for (int x = 0; x < width; ++x) {
dst_row[x] = pal[src_row[x]];
}
}

View File

@@ -25,13 +25,14 @@ namespace Defaults::Video {
constexpr bool FULLSCREEN = false; // Modo de pantalla completa por defecto (false = ventana)
constexpr Screen::Filter FILTER = Screen::Filter::NEAREST; // Filtro por defecto
constexpr bool VERTICAL_SYNC = true; // Vsync activado por defecto
constexpr bool 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 INTEGER_SCALE = true; // Escalado entero activado por defecto
constexpr bool KEEP_ASPECT = true; // Mantener aspecto activado por defecto
constexpr const char* PALETTE_NAME = "zx-spectrum"; // Paleta por defecto
constexpr bool LINEAR_UPSCALE = false; // Upscale NEAREST por defecto
constexpr int DOWNSCALE_ALGO = 1; // Downscale Lanczos2 por defecto
constexpr bool GPU_ACCELERATION = true; // Aceleración GPU activada por defecto
} // namespace Defaults::Video
namespace Defaults::Border {
@@ -100,5 +101,6 @@ namespace Defaults::Game::Player {
constexpr int SPAWN_X = 25 * Tile::SIZE; // Posición X inicial
constexpr int SPAWN_Y = 13 * Tile::SIZE; // Posición Y inicial
constexpr SDL_FlipMode SPAWN_FLIP = Flip::LEFT; // Orientación inicial
constexpr 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

View File

@@ -620,20 +620,26 @@ auto Player::handleKillingTiles() -> bool {
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) {
if (color != 0) {
color_ = color;
return;
}
if (Options::cheats.invincible == Options::Cheat::State::ENABLED) {
color_ = static_cast<Uint8>(PaletteColor::CYAN);
} else if (Options::cheats.infinite_lives == Options::Cheat::State::ENABLED) {
color_ = static_cast<Uint8>(PaletteColor::YELLOW);
// Color personalizado desde opciones
if (Options::game.player_color >= 0) {
color_ = static_cast<Uint8>(Options::game.player_color);
} else {
color_ = static_cast<Uint8>(PaletteColor::WHITE);
}
// Si el color coincide con el fondo de la habitación, usar fallback
if (room_ != nullptr && color_ == room_->getBGColor()) {
color_ = (room_->getBGColor() != static_cast<Uint8>(PaletteColor::WHITE))
? static_cast<Uint8>(PaletteColor::WHITE)
: static_cast<Uint8>(PaletteColor::BRIGHT_BLACK);
}
}
// Actualiza los puntos de colisión
@@ -765,11 +771,18 @@ void Player::applySpawnValues(const SpawnData& spawn) {
sprite_->setFlip(spawn.flip);
}
// Resuelve nombre de skin a fichero de animación
auto Player::skinToAnimationPath(const std::string& skin_name) -> std::string {
if (skin_name == "default") {
return "player.yaml";
}
return skin_name + ".yaml";
}
// Cambia la skin del jugador en caliente preservando la orientación actual
void Player::setSkin(int skin_num) {
void Player::setSkin(const std::string& skin_name) {
const auto FLIP = sprite_->getFlip();
const std::string PATH = (skin_num == 2) ? "player2.yaml" : "player.yaml";
initSprite(PATH);
initSprite(skinToAnimationPath(skin_name));
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_->setWidth(WIDTH);
sprite_->setHeight(HEIGHT);
sprite_->setCurrentAnimation("walk");
sprite_->setCurrentAnimation("default");
}
// 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 getSpawnParams() -> SpawnData { return {.x = x_, .y = y_, .vx = vx_, .vy = vy_, .last_grounded_position = last_grounded_position_, .state = state_, .flip = sprite_->getFlip()}; } // Obtiene el estado de reaparición del jugador
void setColor(Uint8 color = 0); // Establece el color del jugador (0 = automático según cheats)
void setSkin(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
//[[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

View File

@@ -3,10 +3,10 @@
#include <string>
namespace GameControl {
// Disponible en todos los builds — refresca el color del jugador según cheats
inline std::function<void()> refresh_player_color;
// Disponible en todos los builds — cambia la skin del jugador (1=normal, 2=alternativa)
inline std::function<void(int)> change_player_skin;
// Disponible en todos los builds — cambia la skin del jugador ("default" o nombre de enemigo)
inline std::function<void(const std::string&)> change_player_skin;
// Disponible en todos los builds — cambia el color del jugador (-1 = automático, 0-15 = color fijo)
inline std::function<void(int)> change_player_color;
} // namespace GameControl
#ifdef _DEBUG

View File

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

View File

@@ -28,6 +28,7 @@ class Scoreboard {
void render(); // Pinta el objeto en pantalla
void update(float delta_time); // Actualiza las variables del objeto
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
private:
@@ -42,7 +43,23 @@ class Scoreboard {
// 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 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
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 "external/fkyaml_node.hpp" // Para fkyaml::node
#include "game/defaults.hpp" // Para GameDefaults::VERSION
#include "utils/utils.hpp" // Para toLower
namespace Options {
@@ -177,24 +178,6 @@ namespace Options {
{"LEFT_STICK_LEFT", 200},
{"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
auto filterToString(Screen::Filter filter) -> std::string {
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() ---
// Carga configuración de ventana desde YAML
@@ -309,20 +286,91 @@ 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) {
if (!vid.contains("palette")) { return; }
try {
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 (...) {
video.palette = Defaults::Video::PALETTE_NAME;
}
}
// 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) {
// fullscreen (antes era "mode")
if (vid.contains("fullscreen")) {
try {
video.fullscreen = vid["fullscreen"].get_value<bool>();
@@ -331,7 +379,6 @@ namespace Options {
}
}
// filter (ahora es string)
if (vid.contains("filter")) {
try {
auto filter_str = vid["filter"].get_value<std::string>();
@@ -341,47 +388,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")) {
try {
video.vertical_sync = vid["vertical_sync"].get_value<bool>();
@@ -406,30 +412,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);
}
@@ -439,10 +421,18 @@ namespace Options {
const auto& vid = yaml["video"];
loadBasicVideoFieldsFromYaml(vid);
// Lee border
if (vid.contains("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 +510,19 @@ namespace Options {
const auto& player_node = yaml["player"];
if (player_node.contains("skin")) {
try {
int skin = player_node["skin"].get_value<int>();
game.player_skin = (skin == 2) ? 2 : Defaults::Game::Player::SKIN;
game.player_skin = player_node["skin"].get_value<std::string>();
} catch (...) {
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 +678,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
auto saveToFile() -> bool {
// Abre el fichero para escritura
@@ -727,23 +743,27 @@ namespace Options {
file << "# VIDEO \n";
file << "video:\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 << " integer_scale: " << (video.integer_scale ? "true" : "false") << "\n";
file << " keep_aspect: " << (video.keep_aspect ? "true" : "false") << "\n";
file << " linear_upscale: " << (video.linear_upscale ? "true" : "false") << "\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 << " filter: " << filterToString(video.filter) << " # filter: nearest (pixel perfect) | linear (smooth)\n";
file << " palette: " << video.palette << "\n";
file << " border:\n";
file << " enabled: " << (video.border.enabled ? "true" : "false") << "\n";
file << " width: " << video.border.width << "\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";
// KEYBOARD CONTROLS
@@ -765,7 +785,8 @@ namespace Options {
file << "\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 << "# KIOSK MODE\n";
@@ -787,6 +808,40 @@ namespace Options {
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
void setPostFXFile(const std::string& path) {
postfx_file_path = path;
@@ -838,14 +893,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()) {
current_postfx_preset = std::clamp(
current_postfx_preset,
0,
static_cast<int>(postfx_presets.size()) - 1);
resolvePostFXPresetName();
} else {
current_postfx_preset = 0;
video.shader.current_postfx_preset = 0;
}
std::cout << "PostFX file loaded: " << postfx_presets.size() << " preset(s)\n";
@@ -950,7 +1002,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({"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});
current_postfx_preset = 0;
video.shader.current_postfx_preset = 0;
return true;
}
@@ -981,7 +1033,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({"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});
current_crtpi_preset = 0;
video.shader.current_crtpi_preset = 0;
return true;
}
out << "# JailDoctor's Dilemma - CrtPi Shader Presets\n";
@@ -1062,7 +1114,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({"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});
current_crtpi_preset = 0;
video.shader.current_crtpi_preset = 0;
return true;
}
@@ -1121,13 +1173,11 @@ namespace Options {
}
}
// Resolver el nombre del preset a índice
if (!crtpi_presets.empty()) {
current_crtpi_preset = std::clamp(
current_crtpi_preset,
0,
static_cast<int>(crtpi_presets.size()) - 1);
resolveCrtPiPresetName();
} else {
current_crtpi_preset = 0;
video.shader.current_crtpi_preset = 0;
}
std::cout << "CrtPi file loaded: " << crtpi_presets.size() << " preset(s)\n";
@@ -1138,7 +1188,7 @@ namespace Options {
// Cargar defaults en memoria en caso de error
crtpi_presets.clear();
crtpi_presets.push_back({"DEFAULT", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false});
current_crtpi_preset = 0;
video.shader.current_crtpi_preset = 0;
return false;
}
}

View File

@@ -75,21 +75,42 @@ namespace Options {
float height{Defaults::Border::HEIGHT}; // Alto del borde
};
// 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 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
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
// Estructura para las opciones de GPU
struct GPU {
bool acceleration{Defaults::Video::GPU_ACCELERATION}; // Usar aceleración GPU; false = path SDL fallback directo
std::string preferred_driver; // Driver GPU preferido; vacío = auto. Aplica en el próximo arranque.
};
// Estructura para las opciones de supersampling
struct Supersampling {
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)
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
std::string gpu_preferred_driver; // Driver GPU preferido; vacío = auto. Aplica en el próximo arranque.
};
// Estructura para las opciones de shader (dentro de Video)
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 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
@@ -114,9 +135,10 @@ namespace Options {
// Estructura para las opciones de juego
struct Game {
float width{Defaults::Canvas::WIDTH}; // Ancho 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)
float width{Defaults::Canvas::WIDTH}; // Ancho de la resolucion del juego
float height{Defaults::Canvas::HEIGHT}; // Alto de la resolucion del juego
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
@@ -171,17 +193,12 @@ namespace Options {
// --- Variables 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
// --- Variables CrtPi ---
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
// --- Shader activo ---
inline Rendering::ShaderType current_shader{Rendering::ShaderType::POSTFX}; // Shader de post-procesado activo
// --- Funciones públicas ---
void setConfigFile(const std::string& path); // Establece la ruta del fichero de configuración
auto loadFromFile() -> bool; // Carga las opciones desde el fichero configurado
@@ -192,4 +209,7 @@ namespace Options {
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)
void resolvePostFXPresetName(); // Resuelve el nombre del preset PostFX a índice
void resolveCrtPiPresetName(); // Resuelve el nombre del preset CrtPi a índice
} // namespace Options

View File

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

View File

@@ -64,12 +64,16 @@ Game::Game(Mode mode)
Cheevos::get()->enable(!Options::cheats.enabled()); // Deshabilita los logros si hay trucos activados
Cheevos::get()->clearUnobtainableState();
GameControl::refresh_player_color = [this]() -> void { player_->setColor(); };
Console::get()->on_toggle = [this](bool open) { player_->setIgnoreInput(open); };
if (Console::get()->isActive()) { player_->setIgnoreInput(true); }
GameControl::change_player_skin = [this](int skin_num) -> void {
Options::game.player_skin = skin_num;
player_->setSkin(skin_num);
GameControl::change_player_skin = [this](const std::string& skin_name) -> void {
Options::game.player_skin = skin_name;
player_->setSkin(skin_name);
scoreboard_->refreshPlayerSkin();
};
GameControl::change_player_color = [this](int color) -> void {
Options::game.player_color = color;
player_->setColor();
};
#ifdef _DEBUG
@@ -80,9 +84,17 @@ Game::Game(Mode mode)
Options::stats.items = count;
};
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();
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();
scoreboard_data_->music = !Debug::get()->isEnabled();
scoreboard_data_->music ? Audio::get()->resumeMusic() : Audio::get()->pauseMusic();
@@ -116,8 +128,8 @@ Game::Game(Mode mode)
Game::~Game() {
ItemTracker::destroy();
GameControl::refresh_player_color = nullptr;
GameControl::change_player_skin = nullptr;
GameControl::change_player_color = nullptr;
Console::get()->on_toggle = nullptr;
#ifdef _DEBUG
@@ -512,15 +524,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)
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();
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();
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();
scoreboard_data_->music = !Debug::get()->isEnabled();
scoreboard_data_->music ? Audio::get()->resumeMusic() : Audio::get()->pauseMusic();
break;
}
default:
break;
@@ -638,6 +659,9 @@ auto Game::changeRoom(const std::string& room_path) -> bool {
// Pasa la nueva habitación al jugador
player_->setRoom(room_);
// Recalcula el color del jugador (evita coincidir con el fondo)
player_->setColor();
// Cambia la habitación actual
current_room_ = room_path;
@@ -884,7 +908,7 @@ void Game::checkEndGameCheevos() { // NOLINT(readability-convert-member-functio
// Inicializa al jugador
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();
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)};
player_ = std::make_shared<Player>(PLAYER);
if (IGNORE_INPUT) { player_->setIgnoreInput(true); }

View File

@@ -130,5 +130,7 @@ class Game {
// Variables de debug para arrastre con 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)
// Estado previo de invencibilidad antes de entrar en modo debug
bool invincible_before_debug_{false};
#endif
};

View File

@@ -2,43 +2,21 @@
#include <SDL3/SDL.h>
#include <cctype> // Para toupper
#include <functional> // Para function
#include <iostream> // Para std::cout
#include <sstream> // Para std::istringstream
#include <string> // Para string
#include <vector> // Para vector
#include <algorithm> // Para ranges::transform
#include <cctype> // Para toupper
#include <sstream> // Para std::istringstream
#include <string> // Para string
#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/sprite/sprite.hpp" // Para Sprite
#include "core/rendering/surface.hpp" // Para Surface
#include "core/rendering/text.hpp" // Para Text
#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/scene_manager.hpp" // Para SceneManager
#include "game/ui/notifier.hpp" // Para Notifier
#ifdef _DEBUG
#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
};
// ── Helpers de texto ──────────────────────────────────────────────────────────
// Convierte la entrada a uppercase y la divide en tokens por espacios
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;
}
// 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 ───────────────────────────────────────────────────────────────────
// 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_;
y_ = -height_;
// Construir mapa de autocompletado a partir de COMMANDS
for (const auto& cmd : COMMANDS) {
for (const auto& [path, opts] : cmd.completions) {
auto& vec = tab_completions_[std::string(path)];
for (const auto& opt : opts) { vec.emplace_back(opt); }
}
}
// Cargar comandos desde YAML
registry_.load("data/console/commands.yaml");
buildSurface();
}
@@ -1225,36 +350,19 @@ void Console::handleEvent(const SDL_Event& event) {
const size_t space_pos = upper.rfind(' ');
if (space_pos == std::string::npos) {
// Modo comando: ciclar keywords que empiecen por el prefijo
for (const auto& cmd : COMMANDS) {
if (cmd.hidden) { continue; }
if (upper.empty() || cmd.keyword.starts_with(upper)) {
tab_matches_.emplace_back(cmd.keyword);
// Modo comando: ciclar keywords visibles que empiecen por el prefijo
for (const auto& kw : registry_.getVisibleKeywords()) {
if (upper.empty() || kw.starts_with(upper)) {
tab_matches_.emplace_back(kw);
}
}
} else {
const std::string base_cmd = upper.substr(0, space_pos);
const std::string sub_prefix = upper.substr(space_pos + 1);
if (base_cmd == "PALETTE" && Screen::get() != nullptr) {
// NEXT/PREV primero, luego todos los nombres de paleta disponibles
for (const auto* sv : {"NEXT", "PREV"}) {
if (sub_prefix.empty() || std::string_view{sv}.starts_with(sub_prefix)) {
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);
}
}
const auto opts = registry_.getCompletions(base_cmd);
for (const auto& arg : opts) {
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::vector<std::string> ARGS(TOKENS.begin() + 1, TOKENS.end());
std::string result;
bool found = false;
bool instant = false;
for (const auto& command : COMMANDS) {
if (command.keyword == cmd) {
result = command.execute(ARGS);
instant = command.instant;
found = true;
break;
}
}
if (!found) {
const auto* def = registry_.findCommand(cmd);
if (def != nullptr) {
result = registry_.execute(cmd, ARGS);
instant = def->instant;
} else {
std::string cmd_lower = cmd;
std::ranges::transform(cmd_lower, cmd_lower.begin(), ::tolower);
result = "Unknown: " + cmd_lower;

View File

@@ -2,12 +2,13 @@
#include <SDL3/SDL.h>
#include <deque> // Para deque (historial)
#include <functional> // Para function
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <unordered_map> // Para unordered_map (tab_completions_)
#include <vector> // Para vector
#include <deque> // Para deque (historial)
#include <functional> // Para function
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "game/ui/console_commands.hpp" // Para CommandRegistry
class Surface;
class Sprite;
@@ -100,7 +101,9 @@ class Console {
std::string saved_input_; // guarda input_line_ al empezar a navegar
// Estado de autocompletado (TAB)
std::vector<std::string> tab_matches_; // Comandos que coinciden con el prefijo actual
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
std::vector<std::string> tab_matches_; // Comandos que coinciden con el prefijo actual
int tab_index_{-1}; // Índice actual en tab_matches_
// 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 {
constexpr const char* WINDOW_CAPTION = "© 2022 JailDoctor's Dilemma — JailDesigner";
constexpr const char* COPYRIGHT = "@2022 JailDesigner";
constexpr const char* VERSION = "1.11"; // Versión por defecto
constexpr const char* VERSION = "1.12"; // Versión por defecto
} // namespace Texts
// Tamaño de bloque

View File

@@ -414,6 +414,13 @@ auto toUpper(const std::string& str) -> std::string {
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
auto getFileName(const std::string& path) -> std::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 toLower(const std::string& str) -> std::string; // String a minú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
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()