Compare commits

...
26 Commits
Author SHA1 Message Date
JailDesigner babb111b6f chore(version): bump 0.36.0 -> 0.37.0 2026-05-08 09:59:23 +02:00
JailDesignerandClaude Opus 4.7 866e133aa9 Merge branch 'feat/logger'
Sistema de log unificat (Log:: wrapper sobre SDL_Log) substituint
els 176 std::cout/std::cerr dispersos per source/. API basada en
std::format, filtre runtime via AEE_LOG (sintaxi *:info,Tag:debug),
i marcadors literals via Log::raw().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:59:16 +02:00
JailDesignerandClaude Opus 4.7 170521aba6 feat(log): afegeix Log::raw() per a marcadors sense prefix
Recupera el comportament històric dels marcadors "Game start" i "Bye!"
que han de sortir pelats (no com a "App: Game start"). Log::raw(level, msg)
és una variant prima sobre SDL_LogMessage que no anteposa cap tag i cau a
SDL_LOG_CATEGORY_APPLICATION (filtrable per AEE_LOG global).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:54:13 +02:00
JailDesignerandClaude Opus 4.7 c8882a3f6e refactor(log): migra els subsistemes restants i elimina iostream
Acaba la migració dels std::cout/cerr a Log::*:

- core/locale, core/ui/ui_config, core/rendering/{screen,text,gif,
  surface, sprite/animated_sprite, sdl3gpu/*}.
- game/{options, debug_config, editor/editor_config, levels/level_manager,
  map/{map,tilemap,paint_layer}, entities/entity_manager_loadlevel}.

Severitats coherents: warns per a configs absents amb fallback,
errors per a YAML malformat o E/S fallida, infos per a estats.

Resultat:
- 0 occurrences de std::cout/std::cerr a source/.
- Cap #include <iostream> orfà (eliminat de surface.cpp).
- Output funcional idèntic al històric; AEE_LOG filtra (ex.
  `AEE_LOG=warn` només mostra warnings, `AEE_LOG=*:warn,Tag:info`
  combina globals i overrides).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:34:24 +02:00
JailDesignerandClaude Opus 4.7 5fce52e0ad refactor(log): migra game/scenes i game/ui a Log::
- logo_scene, title_scene, intro_scene, room_fsm: missatges de
  cicle d'escena com a Log::info amb tag "scene" o "RoomFsm".
  Eliminats els TODO obsolets sobre el logger pendent.
- game_scene (16 logs): events del bus (LEVEL COMPLETE, PLAYER HIT,
  PLAYER KILLED) com a info; transicions de ROOM com a info; warns
  als casos d'àudio absent o música no arrencada.
- hud (3 logs): init i events com a info.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:24:11 +02:00
JailDesignerandClaude Opus 4.7 0b708f1515 refactor(log): migra core/input i core/audio a Log::
- gamepad_manager.cpp (12 logs): connect/disconnect com a info,
  errors d'open/init com a error, warning per BD no carregada.
- key_config.cpp (5 logs): error per arxius malformats, info
  per al recompte de bindings carregats.
- gamepad_bindings.cpp (4 logs): error/info anàleg.
- audio_effects.cpp (2 errors de format).
- sound_effects_config.cpp (3 logs): warn quan no hi ha presets,
  error de parse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:17:12 +02:00
JailDesignerandClaude Opus 4.7 6e99fd0a8b refactor(log): migra core/resources i core/system a Log::
- resource_pack.cpp i resource_cache.cpp: tots els std::cerr passen a
  Log::error.
- director.cpp: std::cout/cerr/fprintf → Log::info / error / critical
  segons severitat.
- printWithDots (utils.cpp): ara és un wrapper sobre Log::info que
  manté l'amplada total (~50 chars) reproduint l'alineació de punts.
  Els callers pasen el tag sense " : " (ja l'afegeix el logger).
- pack_resources: afegit log.cpp i SDL3 al target perquè
  resource_pack.cpp ara depèn de Log::.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:10:03 +02:00
JailDesignerandClaude Opus 4.7 1243b457be feat(log): introdueix Log:: wrapper sobre SDL_Log
Logger lleuger a source/utils/log.{hpp,cpp} amb API basada en std::format
i filtre runtime via la variable d'entorn AEE_LOG (sintaxi
*:info,Resource::Helper:warn,scene:debug). Substitueix gradualment els
std::cout/cerr dispersos.

- Categories CUSTOM registrades sota demanda; per defecte INFO.
- Categories SDL natives no es toquen tret que l'usuari posi *:level.
- Migra resource_helper.cpp i els marcadors d'inici/fi (main.cpp) com a
  testimoni; la resta de subsistemes es migra en commits següents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:01:09 +02:00
JailDesigner c910a16834 tune(whip): puja swing a 320ms (80ms × 4 frames) 2026-05-07 20:36:15 +02:00
JailDesigner 0ec1a2548a feat(mummy): so mummy_hurt en colpejada de fuet 2026-05-07 20:25:23 +02:00
JailDesigner 4ea8ab7b6d art(whip): actualitza els 4 GIFs direccionals del fuet 2026-05-07 20:12:57 +02:00
JailDesigner 05465b791e refactor(data): reorganitza data/ i centralitza paths a defaults.hpp 2026-05-07 19:28:28 +02:00
JailDesigner 9949096fba chore(rename): aee_2026 -> aee_arcade 2026-05-07 18:39:02 +02:00
JailDesigner ea5762265e chore(version): bump 0.35.0 -> 0.36.0 2026-05-07 16:46:11 +02:00
JailDesigner 78732042cb merge: feature/logo-timeline-redesign 2026-05-07 16:45:34 +02:00
JailDesigner f10671d9c0 feat(logo): timeline lineal de 7 fases amb glitch tail i durades al YAML 2026-05-07 16:44:34 +02:00
JailDesigner 7d5a4cee8e chore(version): bump 0.34.0 -> 0.35.0 2026-05-07 14:07:44 +02:00
JailDesigner d560623e00 merge: feature/logo-scanline-reveal 2026-05-07 14:07:26 +02:00
JailDesigner 3f88dfaaee feat(logo): afegit data/sounds/glitch.wav 2026-05-07 14:07:21 +02:00
JailDesigner e1bd75e00d feat(logo): so glitch.wav sincronitzat amb el scanline reveal 2026-05-07 14:04:48 +02:00
JailDesigner e32cff9dac feat(logo): scanline reveal CRT amb glitch, bursts VHS i pulse de banda 2026-05-07 13:52:57 +02:00
JailDesigner 243ced7498 feat(title): seqüència d'intro temporitzada amb slide-in del peu 2026-05-07 13:08:13 +02:00
JailDesigner fceeb11cdd feat(title): cosmètics i posicions a YAML, font Tiny5 al copyright 2026-05-07 11:51:00 +02:00
JailDesigner 233fcb8226 feat(title): game_logo.gif al lloc del text i font PixeloidSans-Bold per al copyright 2026-05-06 22:35:27 +02:00
JailDesigner 8bc45ccedc feat(logo): FSM amb fade-in/middle/fade-out (500/5000/500 ms) i música del títol a 2,5 s del centre 2026-05-06 20:01:31 +02:00
JailDesigner f7ab6a641a feat(logo,menu): reiniciar des de servei → LOGO amb reset (atura música) i logo_jailgames.gif centrat 2026-05-06 19:54:30 +02:00
120 changed files with 1937 additions and 567 deletions

No files matched your search

+2 -2
View File
@@ -8,8 +8,8 @@ dist/
*.dylib *.dylib
*.dll *.dll
*.exe *.exe
aee_2026 aee_arcade
_aee_2026 _aee_arcade
.vscode/ .vscode/
.idea/ .idea/
.cache/ .cache/
+11 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(aee_2026 VERSION 0.1.0 LANGUAGES CXX) project(aee_arcade VERSION 0.1.0 LANGUAGES CXX)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE) set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
@@ -93,6 +93,7 @@ set(APP_SOURCES
source/core/rendering/sprite/animated_sprite.cpp source/core/rendering/sprite/animated_sprite.cpp
source/core/rendering/sprite/animated_sprite_parser.cpp source/core/rendering/sprite/animated_sprite_parser.cpp
source/core/rendering/effects/heat_haze.cpp source/core/rendering/effects/heat_haze.cpp
source/core/rendering/effects/scan_reveal.cpp
source/core/rendering/transitions/mosaic.cpp source/core/rendering/transitions/mosaic.cpp
source/core/rendering/transitions/fade_to_black.cpp source/core/rendering/transitions/fade_to_black.cpp
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
@@ -103,6 +104,7 @@ set(APP_SOURCES
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
source/core/resources/palette_loader.cpp source/core/resources/palette_loader.cpp
source/utils/easing.cpp source/utils/easing.cpp
source/utils/log.cpp
source/utils/utils.cpp source/utils/utils.cpp
source/game/debug_config.cpp source/game/debug_config.cpp
source/game/options.cpp source/game/options.cpp
@@ -217,8 +219,10 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
add_executable(pack_resources EXCLUDE_FROM_ALL add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
source/utils/log.cpp
) )
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source") target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_link_libraries(pack_resources PRIVATE SDL3::SDL3)
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack --- # --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
@@ -360,9 +364,11 @@ if(BUILD_TESTS)
add_executable(test_entities_smoke add_executable(test_entities_smoke
tests/test_entities_smoke.cpp tests/test_entities_smoke.cpp
tests/hole_player_stubs.cpp tests/hole_player_stubs.cpp
tests/audio_cache_stubs.cpp
source/core/resources/resource_helper.cpp source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
source/game/map/map.cpp source/game/map/map.cpp
source/utils/log.cpp
${ENTITY_SOURCES_BASE} ${ENTITY_SOURCES_BASE}
) )
target_include_directories(test_entities_smoke PRIVATE target_include_directories(test_entities_smoke PRIVATE
@@ -388,9 +394,11 @@ if(BUILD_TESTS)
tests/test_whip_attack.cpp tests/test_whip_attack.cpp
tests/whip_attack_player_stubs.cpp tests/whip_attack_player_stubs.cpp
tests/hole_player_stubs.cpp tests/hole_player_stubs.cpp
tests/audio_cache_stubs.cpp
source/core/resources/resource_helper.cpp source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
source/game/map/map.cpp source/game/map/map.cpp
source/utils/log.cpp
${ENTITY_SOURCES_BASE} ${ENTITY_SOURCES_BASE}
source/game/entities/concrete/whip_attack.cpp source/game/entities/concrete/whip_attack.cpp
) )
@@ -415,6 +423,7 @@ if(BUILD_TESTS)
source/core/input/key_config.cpp source/core/input/key_config.cpp
source/core/resources/resource_helper.cpp source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
source/utils/log.cpp
) )
target_include_directories(test_scene_flow PRIVATE target_include_directories(test_scene_flow PRIVATE
"${CMAKE_SOURCE_DIR}/source" "${CMAKE_SOURCE_DIR}/source"
@@ -454,6 +463,7 @@ if(BUILD_TESTS)
source/core/resources/resource_helper.cpp source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
source/utils/easing.cpp source/utils/easing.cpp
source/utils/log.cpp
) )
target_include_directories(test_game_fsm PRIVATE target_include_directories(test_game_fsm PRIVATE
"${CMAKE_SOURCE_DIR}/source" "${CMAKE_SOURCE_DIR}/source"
+1 -1
View File
@@ -1,4 +1,4 @@
PROJECT := aee_2026 PROJECT := aee_arcade
BUILDDIR := build BUILDDIR := build
# Detecció de plataforma. En Windows CMake defaulteja a "NMake Makefiles" # Detecció de plataforma. En Windows CMake defaulteja a "NMake Makefiles"
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,4 @@
# AEE 2026 — Tecles d'UI (Global Inputs) # AEE ARCADE — Tecles d'UI (Global Inputs)
# #
# Mapeig id → codi SDL (el que reconeix SDL_GetScancodeFromName). Les tecles # Mapeig id → codi SDL (el que reconeix SDL_GetScancodeFromName). Les tecles
# de moviment del jugador viuen a part (Defaults::KeysGame); ací només hi ha # de moviment del jugador viuen a part (Defaults::KeysGame); ací només hi ha
File renamed without changes.
+13 -53
View File
@@ -58,6 +58,15 @@ fonts:
gif: "data/fonts/ari-w9500-condensed-display.gif" gif: "data/fonts/ari-w9500-condensed-display.gif"
fnt: "data/fonts/ari-w9500-condensed-display.fnt" fnt: "data/fonts/ari-w9500-condensed-display.fnt"
kerning: 0 kerning: 0
# Copyright de la TITLE: pixel-art estricte (Tiny5-Regular size 5,
# caixa 3×6 px) generat amb font-gen. Diferenciada de la `placeholder`
# que pinta el "press start" — així la jerarquia tipogràfica del peu
# queda neta.
copyright:
name: "Tiny5-Regular"
gif: "data/fonts/Tiny5-Regular.gif"
fnt: "data/fonts/Tiny5-Regular.fnt"
kerning: 1
hud: hud:
bg_enabled: false bg_enabled: false
@@ -92,7 +101,7 @@ pause:
enter_curve: outCubic enter_curve: outCubic
exit_curve: inCubic exit_curve: inCubic
# Sons del menú de servei. Els fitxers viuen a data/sounds/. SELECT # Sons del menú de servei. Els fitxers viuen a data/audio/sounds/. SELECT
# acompanya navegació (amunt/avall) i back; ACCEPT acompanya modificació # acompanya navegació (amunt/avall) i back; ACCEPT acompanya modificació
# d'opció (esquerra/dreta/retorn) o entrada a un submenú. # d'opció (esquerra/dreta/retorn) o entrada a un submenú.
sounds: sounds:
@@ -101,55 +110,6 @@ sounds:
# Disparat quan un jugador prem START a la TITLE (primera pulsació o # Disparat quan un jugador prem START a la TITLE (primera pulsació o
# la del segon jugador dins de la finestra de STARTING). # la del segon jugador dins de la finestra de STARTING).
player_start: "player_start.wav" player_start: "player_start.wav"
# Soroll/scratch sincronitzat amb la fase SCAN de LogoScene (scanline
# Escena TITLE (placeholder visual mentre no hi haja artwork). # reveal). Es dispara al constructor de l'escena.
# `bg_color` i `text_color` són índexs sobre la paleta "ui" (uzebox.pal): logo_glitch: "glitch.wav"
# 1 = (36,0,0) gairebé negre amb un mínim de càlid; 31 = (255,109,0)
# taronja pur. `duration_ms` és el timeout abans de saltar a la
# següent escena (skipable amb tecla no reservada). Els dos
# `blink_*_ms` defineixen el cicle del prompt "press start": el text
# es veu durant `visible_ms` i s'amaga durant `hidden_ms` repetint-se.
title:
duration_ms: 30000
bg_color: 1
text_color: 31
copyright_color: 246
blink_visible_ms: 800
blink_hidden_ms: 200
# Fase STARTING (entre la primera pulsació de START i el salt a la
# següent escena). `starting_ms` és la durada fixa; durant aquesta
# fase el parpadeig accelera per `starting_blink_speedup` (2.0 = doble
# de freqüència). Si l'altre jugador prem START dins d'aquesta
# finestra, el comptador es reseteja i s'arrenca amb BOTH.
starting_ms: 3000
# Durant STARTING el prompt commuta a un cicle simètric 50/50
# (visible/ocult) amb aquest període total. 200 ms = 5 Hz, prou
# ràpid per llegir-se com a "pulsació confirmada".
starting_blink_period_ms: 200
# Fade-out estil "mosaic dissolve" durant l'última part de STARTING.
# `fade_out_ms` ha de ser <= starting_ms (els últims X ms del STARTING
# es fan amb el dissolve actiu fins a quedar negre). `fade_out_max_block`
# és la mida màxima de bloc en píxels al final del fade.
fade_out_ms: 800
fade_out_max_block: 32
# Fade-out de `title.ogg` en entrar a STARTING. Independent del visual:
# la música pot acabar abans o després del mosaic dissolve.
music_fade_out_ms: 3000
# Fade-to-black per a la sortida d'attract (timeout cap a LOGO). NO és
# mosaic — capa negra lineal damunt del canvas. La música no es toca:
# `title.ogg` continua sonant cap a LOGO, que ja veurà què en fa.
attract_fade_to_black_ms: 800
# Fade-in inicial: capa negra al començament que s'esvaeix cap al fons.
# Mateix valor que `room_intro.fade_in_ms` (data/scenes/game.yaml) perquè
# l'arribada a la TITLE i a una sala de joc tinguin el mateix ritme.
intro_fade_in_ms: 500
# Distorsió sinusoidal del fons (efecte de calor del desert).
# `amplitude_px`: desplaçament màxim de cada fila en píxels.
# `wavelength_px`: distància vertical entre dos pics de l'ona.
# `speed_hz`: velocitat amb què avança la fase (cicles per segon).
# Si `enabled` és false, el fons es pinta sense distorsió.
heat_haze:
enabled: true
amplitude_px: 1.5
wavelength_px: 32.0
speed_hz: 0.6
+3
View File
@@ -58,3 +58,6 @@ hurt:
duration_ms: 600 duration_ms: 600
speed: 60 speed: 60
whippable: true whippable: true
# So opcional: basename del fitxer dins `data/audio/sounds/`. Es dispara
# un sol cop per fuetada quan la mòmia entra a HURT. Si manca, no sona.
sound: mummy_hurt.wav
File renamed without changes.
File renamed without changes.
@@ -40,10 +40,10 @@ animations:
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 } hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true } hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 } attack_south: { frames: [20, 21, 22, 23], frame_ms: 80 }
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 } attack_north: { frames: [24, 25, 26, 27], frame_ms: 80 }
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 } attack_east: { frames: [28, 29, 30, 31], frame_ms: 80 }
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true } attack_west: { frames: [28, 29, 30, 31], frame_ms: 80, flip_h: true }
recover_south: { frames: [23] } recover_south: { frames: [23] }
recover_north: { frames: [27] } recover_north: { frames: [27] }
@@ -119,7 +119,7 @@ hurt:
# recuperació en què la hitbox ja no fa dany. # recuperació en què la hitbox ja no fa dany.
whip: whip:
range_tiles: 3 range_tiles: 3
swing_ms: 200 swing_ms: 320
cooldown_ms: 200 cooldown_ms: 200
damage: 1 damage: 1
sound: whip.wav sound: whip.wav
@@ -136,7 +136,7 @@ death:
# poden ferir ni matar el jugador (els forats sí que el maten — la # poden ferir ni matar el jugador (els forats sí que el maten — la
# caiguda no és dany convencional). Visualment es manifesta amb un # caiguda no és dany convencional). Visualment es manifesta amb un
# parpadeig que alterna la paleta normal del sprite amb la paleta # parpadeig que alterna la paleta normal del sprite amb la paleta
# `palette` (basename de `data/player/<name>.pal`) cada # `palette` (basename de `data/entities/player/<name>.pal`) cada
# `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la # `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la
# mecànica queda desactivada i el comportament és l'antic. # mecànica queda desactivada i el comportament és l'antic.
immunity: immunity:
File renamed without changes.
@@ -40,10 +40,10 @@ animations:
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 } hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true } hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 } attack_south: { frames: [20, 21, 22, 23], frame_ms: 80 }
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 } attack_north: { frames: [24, 25, 26, 27], frame_ms: 80 }
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 } attack_east: { frames: [28, 29, 30, 31], frame_ms: 80 }
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true } attack_west: { frames: [28, 29, 30, 31], frame_ms: 80, flip_h: true }
recover_south: { frames: [23] } recover_south: { frames: [23] }
recover_north: { frames: [27] } recover_north: { frames: [27] }
@@ -119,7 +119,7 @@ hurt:
# recuperació en què la hitbox ja no fa dany. # recuperació en què la hitbox ja no fa dany.
whip: whip:
range_tiles: 3 range_tiles: 3
swing_ms: 200 swing_ms: 320
cooldown_ms: 200 cooldown_ms: 200
damage: 1 damage: 1
sound: whip.wav sound: whip.wav
@@ -136,7 +136,7 @@ death:
# poden ferir ni matar el jugador (els forats sí que el maten — la # poden ferir ni matar el jugador (els forats sí que el maten — la
# caiguda no és dany convencional). Visualment es manifesta amb un # caiguda no és dany convencional). Visualment es manifesta amb un
# parpadeig que alterna la paleta normal del sprite amb la paleta # parpadeig que alterna la paleta normal del sprite amb la paleta
# `palette` (basename de `data/player/<name>.pal`) cada # `palette` (basename de `data/entities/player/<name>.pal`) cada
# `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la # `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la
# mecànica queda desactivada i el comportament és l'antic. # mecànica queda desactivada i el comportament és l'antic.
immunity: immunity:
+2 -2
View File
@@ -25,7 +25,7 @@ sprites:
# Animació compartida entre les 4 direccions: la seqüència de frames i les # Animació compartida entre les 4 direccions: la seqüència de frames i les
# durades són idèntiques; només canvia quina surface/mida es clipa. # durades són idèntiques; només canvia quina surface/mida es clipa.
# - `frames` : índex de tile dins l'spritesheet (graella deduïda). # - `frames` : índex de tile dins l'spritesheet (graella deduïda).
# - `frame_ms` : durada uniforme per frame; 50 ms × 4 ≈ 200 ms swing. # - `frame_ms` : durada uniforme per frame; 80 ms × 4 = 320 ms swing.
# - `loop: false` : el látigo viu una animació i mor (`markDead`). # - `loop: false` : el látigo viu una animació i mor (`markDead`).
# - `active_frames` : valors dins de `frames` durant els quals la # - `active_frames` : valors dins de `frames` durant els quals la
# hitbox aplica dany. Wind-up (0) i recovery (3) no # hitbox aplica dany. Wind-up (0) i recovery (3) no
@@ -33,6 +33,6 @@ sprites:
animations: animations:
swing: swing:
frames: [0, 1, 2, 3] frames: [0, 1, 2, 3]
frame_ms: 50 frame_ms: 80
loop: false loop: false
active_frames: [1, 2] active_frames: [1, 2]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 B

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 327 B

+133
View File
@@ -0,0 +1,133 @@
# Font: Tiny5-Regular — generado desde Tiny5-Regular.ttf size 8
# Generado con font_gen.py
box_width 6
box_height 9
columns 15
# codepoint_decimal ancho_visual
32 2 # U+0020
33 1 # !
34 3 # "
35 5 # #
36 3 # $
37 4 # %
38 4 # &
39 1 # '
40 2 # (
41 2 # )
42 3 # *
43 3 # +
44 2 # ,
45 3 # -
46 1 # .
47 2 # /
48 3 # 0
49 2 # 1
50 3 # 2
51 3 # 3
52 3 # 4
53 3 # 5
54 3 # 6
55 3 # 7
56 3 # 8
57 3 # 9
58 1 # :
59 2 # ;
60 3 # <
61 3 # =
62 3 # >
63 3 # ?
64 4 # @
65 4 # A
66 4 # B
67 4 # C
68 4 # D
69 4 # E
70 4 # F
71 4 # G
72 4 # H
73 1 # I
74 3 # J
75 4 # K
76 3 # L
77 5 # M
78 4 # N
79 4 # O
80 4 # P
81 4 # Q
82 4 # R
83 4 # S
84 3 # T
85 4 # U
86 3 # V
87 5 # W
88 3 # X
89 3 # Y
90 3 # Z
91 2 # [
92 2 # \
93 2 # ]
94 3 # ^
95 3 # _
96 2 # `
97 3 # a
98 3 # b
99 3 # c
100 3 # d
101 3 # e
102 2 # f
103 3 # g
104 3 # h
105 1 # i
106 1 # j
107 3 # k
108 1 # l
109 5 # m
110 3 # n
111 3 # o
112 3 # p
113 3 # q
114 2 # r
115 3 # s
116 2 # t
117 3 # u
118 3 # v
119 5 # w
120 3 # x
121 3 # y
122 3 # z
123 3 # {
124 1 # |
125 3 # }
126 2 # ~
192 4 # À
193 4 # Á
200 4 # È
201 4 # É
204 2 # Ì
205 2 # Í
210 4 # Ò
211 4 # Ó
217 4 # Ù
218 4 # Ú
209 4 # Ñ
199 4 # Ç
224 3 # à
225 3 # á
232 3 # è
233 3 # é
236 2 # ì
237 2 # í
242 3 # ò
243 3 # ó
249 3 # ù
250 3 # ú
241 3 # ñ
231 3 # ç
161 1 # ¡
191 3 # ¿
171 5 # «
187 5 # »
183 1 # ·
169 3 # ©
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
# AEE 2026 — Català # AEE ARCADE — Català
# lang: ca # lang: ca
notification: notification:
+1 -1
View File
@@ -1,4 +1,4 @@
# AEE 2026 — English # AEE ARCADE — English
# lang: en # lang: en
notification: notification:
@@ -1,6 +1,6 @@
# Configuració de la GameScene: timings i corbes de la FSM de sala # Configuració de la GameScene: timings i corbes de la FSM de sala
# (ROOM_INTRO i ROOM_OUTRO). Tots els valors són ms i tota la geometria # (ROOM_INTRO i ROOM_OUTRO). Tots els valors són ms i tota la geometria
# i font del text es reutilitzen del tema UI (data/ui.yaml > pause + # i font del text es reutilitzen del tema UI (data/config/ui.yaml > pause +
# notification). Si manca el fitxer o alguna clau, GameScene cau als # notification). Si manca el fitxer o alguna clau, GameScene cau als
# fallbacks compilats; veure `readSceneConfig` a game_scene.cpp. # fallbacks compilats; veure `readSceneConfig` a game_scene.cpp.
@@ -17,7 +17,7 @@ room_intro:
# single-player s'usa només `player_spawn_delay_ms`. # single-player s'usa només `player_spawn_delay_ms`.
player_spawn_delay_ms: 0 player_spawn_delay_ms: 0
player2_spawn_delay_ms: 300 player2_spawn_delay_ms: 300
# Preset de reverb (catàleg a `data/sounds.yaml`) aplicat al primer # Preset de reverb (catàleg a `data/config/sounds.yaml`) aplicat al primer
# LAND de cada jugador durant la cinemàtica d'entrada. La sala dona # LAND de cada jugador durant la cinemàtica d'entrada. La sala dona
# "més detall" sonor a l'INTRO; els salts del joc i els respawns # "més detall" sonor a l'INTRO; els salts del joc i els respawns
# post-mort cauen al so sec. Buit o absent = sense reverb. # post-mort cauen al so sec. Buit o absent = sense reverb.
@@ -33,7 +33,7 @@ room_outro:
text: text:
# Noms d'easing tal com els resol UiConfig::curveByName (vegeu # Noms d'easing tal com els resol UiConfig::curveByName (vegeu
# `data/ui.yaml`). El text entra suau i surt accelerant. # `data/config/ui.yaml`). El text entra suau i surt accelerant.
enter_curve: outCubic enter_curve: outCubic
exit_curve: inCubic exit_curve: inCubic
+24
View File
@@ -0,0 +1,24 @@
# Configuració de la LogoScene (logotip de l'editora). El revelat del
# logo a la fase SCAN imita un escaneig CRT: una banda lluminosa baixa
# de dalt a baix; per damunt es veu la imatge ja "impressa", per sota
# encara és negre. Al voltant de la banda hi ha glitch (offset
# horitzontal de files amb falloff per distància) i ocasionalment
# apareixen bursts VHS (franges desplaçades) damunt la part ja
# revelada. Quan la banda toca el peu del logo, un flash blanc tanca
# l'efecte i s'entra a MIDDLE (logo estable).
logo:
scan:
duration_ms: 1500 # durada total del scan top→bottom
band_height_px: 4 # alçada (px) de la banda lluminosa
band_alpha: 200 # opacitat (0..255) de la capa blanca de la banda
band_pulse_hz: 20.0 # freqüència del pulse de brillantor (Hz)
band_pulse_depth: 0.45 # profunditat del pulse (0..1; 0 = sense pulse)
# Glitch d'impacte (files al voltant de la banda)
glitch_amplitude_px: 6 # offset horitzontal màxim a la banda
glitch_band_radius_px: 12 # files damunt/sota la banda amb glitch (falloff lineal)
# Bursts VHS sobre la part ja revelada
vhs_burst_per_sec: 3 # bursts/segon mitjà
vhs_burst_duration_ms: 80 # durada de cada burst
vhs_burst_height_px: 8 # alçada de la franja afectada
vhs_burst_amplitude_px: 8 # offset horitzontal del burst
flash_ms: 100 # flash blanc final quan acaba el scan
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

+77
View File
@@ -0,0 +1,77 @@
# Configuració de la TitleScene.
# `bg_color` i `text_color` són índexs sobre la paleta "ui" (uzebox.pal):
# 1 = (36,0,0) gairebé negre amb un mínim de càlid; 31 = (255,109,0)
# taronja pur. `duration_ms` és el timeout abans de saltar a la
# següent escena (skipable amb tecla no reservada). Els dos
# `blink_*_ms` defineixen el cicle del prompt "press start": el text
# es veu durant `visible_ms` i s'amaga durant `hidden_ms` repetint-se.
title:
duration_ms: 30000
bg_color: 1
text_color: 31
# Color del text del © de copyright (peu de la TITLE). Off-white càlid:
# índex 183 = (255,218,170), tipus crema/peach, ~90% blanc amb tint càlid.
copyright_color: 183
# Color de l'outline del prompt "PRESS START". Marró fosc càlid (índex
# 19 = 109,72,0), coherent amb la paleta del logo. Es manté separat de
# `copyright_color` perquè l'outline i el text del © tenen rols visuals
# diferents (contrast vs. peu informatiu).
press_start_outline_color: 19
# Posicions Y dels tres elements verticals de la TITLE, en percentatge
# sobre l'alçada del canvas (240 px). El logo s'ancora pel seu top
# (la imatge cau cap avall des d'aquest Y); el prompt i el copyright
# es centren verticalment sobre la línia.
logo_y_percent: 15
prompt_y_percent: 68
copyright_y_percent: 92
# Separació vertical en píxels entre el minilogo i el text del © al peu.
mini_logo_gap_px: 2
blink_visible_ms: 800
blink_hidden_ms: 200
# Fase STARTING (entre la primera pulsació de START i el salt a la
# següent escena). `starting_ms` és la durada fixa; durant aquesta
# fase el parpadeig accelera per `starting_blink_speedup` (2.0 = doble
# de freqüència). Si l'altre jugador prem START dins d'aquesta
# finestra, el comptador es reseteja i s'arrenca amb BOTH.
starting_ms: 3000
# Durant STARTING el prompt commuta a un cicle simètric 50/50
# (visible/ocult) amb aquest període total. 200 ms = 5 Hz, prou
# ràpid per llegir-se com a "pulsació confirmada".
starting_blink_period_ms: 200
# Fade-out estil "mosaic dissolve" durant l'última part de STARTING.
# `fade_out_ms` ha de ser <= starting_ms (els últims X ms del STARTING
# es fan amb el dissolve actiu fins a quedar negre). `fade_out_max_block`
# és la mida màxima de bloc en píxels al final del fade.
fade_out_ms: 800
fade_out_max_block: 32
# Fade-out de `title.ogg` en entrar a STARTING. Independent del visual:
# la música pot acabar abans o després del mosaic dissolve.
music_fade_out_ms: 3000
# Fade-to-black per a la sortida d'attract (timeout cap a LOGO). NO és
# mosaic — capa negra lineal damunt del canvas. La música no es toca:
# `title.ogg` continua sonant cap a LOGO, que ja veurà què en fa.
attract_fade_to_black_ms: 800
# Seqüència d'aparició dels elements de la TITLE en fase IDLE. Tots
# els temps són absoluts des de l'entrada a l'escena. `fade_in_ms` és
# la capa negra inicial que s'esvaeix cap al fons; els altres marquen
# quan apareix cada element. Si l'usuari prem START en qualsevol
# moment, la seqüència es completa instantàniament.
intro:
fade_in_ms: 500 # fade-in negre inicial sobre el fons
game_logo_at_ms: 1500 # visibilitat binària del game_logo
mini_logo_at_ms: 2000 # inici del slide-in del mini-logo
mini_logo_slide_ms: 500 # durada del slide
copyright_at_ms: 2200 # inici del slide-in del copyright
copyright_slide_ms: 500 # durada del slide
slide_curve: outCubic # corba compartida per mini_logo i copyright
press_start_at_ms: 3000 # quan apareix PRESS START amb parpadeig
# Distorsió sinusoidal del fons (efecte de calor del desert).
# `amplitude_px`: desplaçament màxim de cada fila en píxels.
# `wavelength_px`: distància vertical entre dos pics de l'ona.
# `speed_hz`: velocitat amb què avança la fase (cicles per segon).
# Si `enabled` és false, el fons es pinta sense distorsió.
heat_haze:
enabled: true
amplitude_px: 1.5
wavelength_px: 32.0
speed_hz: 0.6
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

+2 -2
View File
@@ -22,8 +22,8 @@ real (logo animat, menú de títol, cutscene).
Fitxer personal de desenvolupament, **fora del repo**. Viu a la mateixa carpeta Fitxer personal de desenvolupament, **fora del repo**. Viu a la mateixa carpeta
de sistema que `config.yaml`: de sistema que `config.yaml`:
- Linux (XDG): `$XDG_CONFIG_HOME/jailgames/aee_2026/debug.yaml` - Linux (XDG): `$XDG_CONFIG_HOME/jailgames/aee_arcade/debug.yaml`
- Per defecte: `~/.config/jailgames/aee_2026/debug.yaml` - Per defecte: `~/.config/jailgames/aee_arcade/debug.yaml`
### Comportament segons build ### Comportament segons build
+4 -4
View File
@@ -1,6 +1,6 @@
# Sistema d'entitats # Sistema d'entitats
Registre del disseny, els treballs fets i els pendents per al sistema d'entitats (sarcòfags, porta, momia, antorxa, trampes, roca) d'aee_2026. Material de referència per a decisions futures — no és documentació d'API. Registre del disseny, els treballs fets i els pendents per al sistema d'entitats (sarcòfags, porta, momia, antorxa, trampes, roca) d'aee_arcade. Material de referència per a decisions futures — no és documentació d'API.
Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`. Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`.
@@ -24,7 +24,7 @@ Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`.
## 1. Context i objectiu inicial ## 1. Context i objectiu inicial
aee_2026 és un clon tipus Oh Mummy! amb estètica Amidar. Els sarcòfags eren fins ara rectangles purament geomètrics calculats a [Map::getSarcRect](../source/game/map/map.cpp#L67); no hi havia entitats de joc (momies, antorxes, trampes, portes). aee_arcade és un clon tipus Oh Mummy! amb estètica Amidar. Els sarcòfags eren fins ara rectangles purament geomètrics calculats a [Map::getSarcRect](../source/game/map/map.cpp#L67); no hi havia entitats de joc (momies, antorxes, trampes, portes).
**Objectiu inicial** (prompt de partida): **Objectiu inicial** (prompt de partida):
@@ -158,7 +158,7 @@ Al CMakeLists: `ENTITY_SOURCES` conté els fitxers core; `APP_SOURCES` afegeix `
- `make build`: OK, 0 warnings nous. - `make build`: OK, 0 warnings nous.
- `./build/test_entities_smoke`: **9/9 OK**. - `./build/test_entities_smoke`: **9/9 OK**.
- `ctest --output-on-failure`: 1/1 suite passed. - `ctest --output-on-failure`: 1/1 suite passed.
- `timeout 5 xvfb-run ./build/aee_2026`: 5s sense crash, stdout inclou: - `timeout 5 xvfb-run ./build/aee_arcade`: 5s sense crash, stdout inclou:
``` ```
Surface : torch.gif ... LOADED Surface : torch.gif ... LOADED
Surface : sarcophagus.gif ... LOADED (×2) Surface : sarcophagus.gif ... LOADED (×2)
@@ -323,7 +323,7 @@ Dos events encadenats perquè el *pintat* no és la *presència*: es filtra per
- `make debug` net, sense warnings ni de clang-tidy ni de cppcheck als fitxers nous. - `make debug` net, sense warnings ni de clang-tidy ni de cppcheck als fitxers nous.
- `./build/test_entities_smoke`: **ALL OK** (14/14). T9 adaptat (publica `tile_painted` en lloc d'`on_tile_entered`); la resta de tests del sistema d'entitats passen sense tocar. - `./build/test_entities_smoke`: **ALL OK** (14/14). T9 adaptat (publica `tile_painted` en lloc d'`on_tile_entered`); la resta de tests del sistema d'entitats passen sense tocar.
- `timeout 5 xvfb-run ./build/aee_2026`: arrenca, log `PaintLayer: 21x15 tiles (336x240 px), tileset data/tilesets/tomb.gif` després de `EntityManager: 27 entitats carregades`, surt amb `Bye!` sense crash. - `timeout 5 xvfb-run ./build/aee_arcade`: arrenca, log `PaintLayer: 21x15 tiles (336x240 px), tileset data/tilesets/tomb.gif` després de `EntityManager: 27 entitats carregades`, surt amb `Bye!` sense crash.
### Pendent derivat ### Pendent derivat
+4 -4
View File
@@ -1,7 +1,7 @@
# Pla d'implementació — Menú de servei # Pla d'implementació — Menú de servei
Pla per a portar el menú de servei de `../aee` (`source/core/rendering/menu.cpp`) Pla per a portar el menú de servei de `../aee` (`source/core/rendering/menu.cpp`)
a `aee_2026`, adaptat a l'arquitectura actual (Scenes + Director instanciat + a `aee_arcade`, adaptat a l'arquitectura actual (Scenes + Director instanciat +
`Options` amb setters persistents + `Screen` ARGB + `Text` bitmap). Document de `Options` amb setters persistents + `Screen` ARGB + `Text` bitmap). Document de
referència per a implementar-ho en una sessió futura; no s'ha escrit cap codi referència per a implementar-ho en una sessió futura; no s'ha escrit cap codi
encara. encara.
@@ -33,7 +33,7 @@ Tot el que segueix és la base; la còpia mental serveix per a no reinventar res
## 2. Diferències d'arquitectura rellevants ## 2. Diferències d'arquitectura rellevants
| Punt | `../aee` | `aee_2026` | | Punt | `../aee` | `aee_arcade` |
| ----------------- | ---------------------------------------------------- | -------------------------------------------------------------------------- | | ----------------- | ---------------------------------------------------- | -------------------------------------------------------------------------- |
| Overlay | `Overlay::render()` dibuixa Menú + Notifier al buffer | No hi ha Overlay. Cada Scene crida `Notifier::render()` i `RenderInfo::render()` | | Overlay | `Overlay::render()` dibuixa Menú + Notifier al buffer | No hi ha Overlay. Cada Scene crida `Notifier::render()` i `RenderInfo::render()` |
| Director | Singleton `Director::get()` amb `requestQuit/requestRestart` | Classe instanciada; sortida via `SDL_EVENT_QUIT` a `EventBuffer` | | Director | Singleton `Director::get()` amb `requestQuit/requestRestart` | Classe instanciada; sortida via `SDL_EVENT_QUIT` a `EventBuffer` |
@@ -223,7 +223,7 @@ es necessita.
### Limitació: pàgina Controls diferida ### Limitació: pàgina Controls diferida
El menú de `../aee` té una pàgina Controls (`buildControls`) que permet El menú de `../aee` té una pàgina Controls (`buildControls`) que permet
remappar `up/down/left/right` i la tecla de menú. A `aee_2026`: remappar `up/down/left/right` i la tecla de menú. A `aee_arcade`:
- Les tecles de joc són `Defaults::KeysGame::{UP,DOWN,LEFT,RIGHT}` — constants - Les tecles de joc són `Defaults::KeysGame::{UP,DOWN,LEFT,RIGHT}` — constants
`constexpr`, no mutables. `constexpr`, no mutables.
@@ -273,7 +273,7 @@ y, w, h, argb, alpha)` que opere sobre `canvas_.data()`. El canvas és
fons del menú pot ser `alpha=220` i es veu el joc apagat per darrere. fons del menú pot ser `alpha=220` i es veu el joc apagat per darrere.
Mida fixa recomanada per al MVP: `BOX_W = 220`, altura dinàmica (igual que Mida fixa recomanada per al MVP: `BOX_W = 220`, altura dinàmica (igual que
`../aee`). Canvas lògic d'`aee_2026` és `GameCanvas::WIDTH`/`HEIGHT` — `../aee`). Canvas lògic d'`aee_arcade` és `GameCanvas::WIDTH`/`HEIGHT` —
confirmar mida real (mateixos 320×200?) a `utils/defines.hpp` abans de confirmar mida real (mateixos 320×200?) a `utils/defines.hpp` abans de
hardcoded. hardcoded.
+3 -3
View File
@@ -7,17 +7,17 @@
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Aventures en Egipte ARCADE</string> <string>Aventures en Egipte ARCADE</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>aee_2026</string> <string>aee_arcade</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>icon</string> <string>icon</string>
<key>CFBundleIconName</key> <key>CFBundleIconName</key>
<string>icon</string> <string>icon</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>org.jailgames.aee_2026</string> <string>org.jailgames.aee_arcade</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>aee_2026</string> <string>aee_arcade</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
@@ -1,2 +1,2 @@
// aee_2026.rc // aee_arcade.rc
IDI_ICON1 ICON "release/icons/icon.ico" IDI_ICON1 ICON "release/icons/icon.ico"
+1 -1
View File
@@ -80,7 +80,7 @@ class Audio {
// --- Control de sons --- // --- Control de sons ---
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja) void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja) void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
// Reprodueix un so processat per un efecte definit a data/sounds.yaml // Reprodueix un so processat per un efecte definit a data/config/sounds.yaml
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix // (preset_name busca a SoundEffectsConfig). Si el preset no existeix
// o el motor està al cap de canals amb efecte, fa fallback a playSound // o el motor està al cap de canals amb efecte, fa fallback a playSound
// sec — l'usuari sent el so igualment, sense la cua. // sec — l'usuari sent el so igualment, sense la cua.
+3 -3
View File
@@ -5,10 +5,10 @@
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <iostream>
#include <vector> #include <vector>
#include "core/audio/jail_audio.hpp" #include "core/audio/jail_audio.hpp"
#include "utils/log.hpp"
namespace AudioEffects { namespace AudioEffects {
@@ -46,11 +46,11 @@ namespace AudioEffects {
} else if (spec.format == SDL_AUDIO_U8) { } else if (spec.format == SDL_AUDIO_U8) {
bytes_per_sample = 1; bytes_per_sample = 1;
} else { } else {
std::cerr << "AudioEffects: format de so no suportat (només U8 o S16)\n"; Log::error("AudioEffects", "format de so no suportat (només U8 o S16)");
return false; return false;
} }
if (spec.channels < 1 || spec.channels > 2) { if (spec.channels < 1 || spec.channels > 2) {
std::cerr << "AudioEffects: el so ha de ser mono o estèreo\n"; Log::error("AudioEffects", "el so ha de ser mono o estèreo");
return false; return false;
} }
+4 -4
View File
@@ -1,10 +1,10 @@
#include "core/audio/sound_effects_config.hpp" #include "core/audio/sound_effects_config.hpp"
#include <iostream>
#include <string> #include <string>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -27,7 +27,7 @@ auto SoundEffectsConfig::get() -> SoundEffectsConfig& {
void SoundEffectsConfig::load(const std::string& file_path) { void SoundEffectsConfig::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path); auto bytes = Resource::Helper::loadFile(file_path);
if (bytes.empty()) { if (bytes.empty()) {
std::cerr << "SoundEffectsConfig: no s'ha pogut obrir " << file_path << " — cap preset d'efecte disponible\n"; Log::warn("SoundEffectsConfig", "no s'ha pogut obrir {} — cap preset d'efecte disponible", file_path);
return; return;
} }
@@ -60,9 +60,9 @@ void SoundEffectsConfig::load(const std::string& file_path) {
} }
} }
std::cout << "SoundEffectsConfig: " << echoes_.size() << " preset(s) d'eco i " << reverbs_.size() << " de reverb des de " << file_path << '\n'; Log::info("SoundEffectsConfig", "{} preset(s) d'eco i {} de reverb des de {}", echoes_.size(), reverbs_.size(), file_path);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "SoundEffectsConfig: error parsejant " << file_path << ": " << e.what() << '\n'; Log::error("SoundEffectsConfig", "error parsejant {}: {}", file_path, e.what());
} }
} }
+1 -1
View File
@@ -5,7 +5,7 @@
#include "core/audio/jail_audio.hpp" // Per a Ja::EchoParams / Ja::ReverbParams #include "core/audio/jail_audio.hpp" // Per a Ja::EchoParams / Ja::ReverbParams
// Catàleg de presets d'efectes carregat des de data/sounds.yaml. La capa // Catàleg de presets d'efectes carregat des de data/config/sounds.yaml. La capa
// Audio (playSoundWithEcho/playSoundWithReverb) hi accedeix per nom: si el // Audio (playSoundWithEcho/playSoundWithReverb) hi accedeix per nom: si el
// preset no existeix, el so es reprodueix sec amb un avís a stderr. // preset no existeix, el so es reprodueix sec amb un avís a stderr.
// //
+7 -10
View File
@@ -3,12 +3,12 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <fstream> #include <fstream>
#include <iostream>
#include <sstream> #include <sstream>
#include <string_view> #include <string_view>
#include <utility> #include <utility>
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -90,7 +90,7 @@ auto GamepadBindings::get() -> GamepadBindings& {
} }
auto GamepadBindings::defaultButton(Action action) -> int { auto GamepadBindings::defaultButton(Action action) -> int {
// Defaults històrics de l'aee_2026 (vegeu Player::accumulateGamepadMovement // Defaults històrics de l'aee_arcade (vegeu Player::accumulateGamepadMovement
// i les consultes del Director per al menú). // i les consultes del Director per al menú).
switch (action) { switch (action) {
case Action::JUMP: case Action::JUMP:
@@ -167,8 +167,7 @@ void GamepadBindings::load(const std::string& file_path) {
try { try {
auto yaml = fkyaml::node::deserialize(CONTENT); auto yaml = fkyaml::node::deserialize(CONTENT);
if (!yaml.contains("gamepads") || !yaml["gamepads"].is_sequence()) { if (!yaml.contains("gamepads") || !yaml["gamepads"].is_sequence()) {
std::cerr << "GamepadBindings: secció 'gamepads' absent o invàlida a " Log::error("GamepadBindings", "secció 'gamepads' absent o invàlida a {}", file_path);
<< file_path << '\n';
return; return;
} }
for (const auto& entry : yaml["gamepads"]) { for (const auto& entry : yaml["gamepads"]) {
@@ -188,11 +187,9 @@ void GamepadBindings::load(const std::string& file_path) {
} }
configs_.push_back(std::move(cfg)); configs_.push_back(std::move(cfg));
} }
std::cout << "GamepadBindings: " << configs_.size() << " mandos carregats de " Log::info("GamepadBindings", "{} mandos carregats de {}", configs_.size(), file_path);
<< file_path << '\n';
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "GamepadBindings: error parsejant " << file_path << ": " Log::error("GamepadBindings", "error parsejant {}: {}", file_path, e.what());
<< e.what() << '\n';
} }
} }
@@ -200,7 +197,7 @@ auto GamepadBindings::save() -> bool {
if (file_path_.empty()) { return false; } if (file_path_.empty()) { return false; }
std::ostringstream out; std::ostringstream out;
out << "# AEE 2026 - Vinculacions de mando per dispositiu.\n"; out << "# AEE ARCADE - Vinculacions de mando per dispositiu.\n";
out << "# Auto-generat des del menú de servei. Editable a mà.\n"; out << "# Auto-generat des del menú de servei. Editable a mà.\n";
out << "gamepads:\n"; out << "gamepads:\n";
for (const auto& cfg : configs_) { for (const auto& cfg : configs_) {
@@ -215,7 +212,7 @@ auto GamepadBindings::save() -> bool {
std::ofstream file(file_path_, std::ios::binary); std::ofstream file(file_path_, std::ios::binary);
if (!file) { if (!file) {
std::cerr << "GamepadBindings: no es pot escriure " << file_path_ << '\n'; Log::error("GamepadBindings", "no es pot escriure {}", file_path_);
return false; return false;
} }
file << out.str(); file << out.str();
+12 -20
View File
@@ -1,13 +1,13 @@
#include "core/input/gamepad_manager.hpp" #include "core/input/gamepad_manager.hpp"
#include <algorithm> #include <algorithm>
#include <iostream>
#include <iterator> #include <iterator>
#include <string> #include <string>
#include <utility> #include <utility>
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/notification_presets.hpp" #include "core/rendering/notification_presets.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -94,8 +94,7 @@ void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
if (!SDL_IsGamepad(jid)) { return; } if (!SDL_IsGamepad(jid)) { return; }
SDL_Gamepad* pad = SDL_OpenGamepad(jid); SDL_Gamepad* pad = SDL_OpenGamepad(jid);
if (pad == nullptr) { if (pad == nullptr) {
std::cout << "GamepadManager: SDL_OpenGamepad(" << jid Log::error("GamepadManager", "SDL_OpenGamepad({}) ha fallat: {}", jid, SDL_GetError());
<< ") ha fallat: " << SDL_GetError() << '\n';
return; return;
} }
std::string name = uniqueName(prettyName(SDL_GetGamepadName(pad))); std::string name = uniqueName(prettyName(SDL_GetGamepadName(pad)));
@@ -104,8 +103,7 @@ void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
// reconnexió es resoldrà per nom. // reconnexió es resoldrà per nom.
const char* raw_path = SDL_GetGamepadPath(pad); const char* raw_path = SDL_GetGamepadPath(pad);
std::string path = (raw_path != nullptr) ? raw_path : ""; std::string path = (raw_path != nullptr) ? raw_path : "";
std::cout << "GamepadManager: connectat " << name Log::info("GamepadManager", "connectat {}{}", name, path.empty() ? std::string{} : " [" + path + "]");
<< (path.empty() ? "" : " [" + path + "]") << '\n';
entries_.push_back(Entry{.pad = pad, .instance_id = jid, .pretty_name = name, .path = path}); entries_.push_back(Entry{.pad = pad, .instance_id = jid, .pretty_name = name, .path = path});
// Hot-plug: si alguna assignació pendent encaixa amb aquest mando, // Hot-plug: si alguna assignació pendent encaixa amb aquest mando,
@@ -129,7 +127,7 @@ void GamepadManager::removeGamepad(SDL_JoystickID jid) {
std::string name = std::move(it->pretty_name); std::string name = std::move(it->pretty_name);
SDL_Gamepad* pad = it->pad; SDL_Gamepad* pad = it->pad;
if (pad != nullptr) { SDL_CloseGamepad(pad); } if (pad != nullptr) { SDL_CloseGamepad(pad); }
std::cout << "GamepadManager: desconnectat " << name << '\n'; Log::info("GamepadManager", "desconnectat {}", name);
entries_.erase(it); entries_.erase(it);
// Qualsevol assignació que apuntava a aquest mando queda amb `pad` // Qualsevol assignació que apuntava a aquest mando queda amb `pad`
@@ -145,8 +143,7 @@ void GamepadManager::removeGamepad(SDL_JoystickID jid) {
void GamepadManager::init() { void GamepadManager::init() {
if (initialized_) { return; } if (initialized_) { return; }
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) { if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
std::cout << "GamepadManager: SDL_INIT_GAMEPAD ha fallat: " Log::error("GamepadManager", "SDL_INIT_GAMEPAD ha fallat: {}", SDL_GetError());
<< SDL_GetError() << '\n';
return; return;
} }
// `gamecontrollerdb.txt` és opcional. Viu a l'arrel del projecte // `gamecontrollerdb.txt` és opcional. Viu a l'arrel del projecte
@@ -161,10 +158,9 @@ void GamepadManager::init() {
} }
const int ADDED = SDL_AddGamepadMappingsFromFile(db_path.c_str()); const int ADDED = SDL_AddGamepadMappingsFromFile(db_path.c_str());
if (ADDED < 0) { if (ADDED < 0) {
std::cout << "GamepadManager: no s'ha pogut carregar " << db_path Log::warn("GamepadManager", "no s'ha pogut carregar {}: {}", db_path, SDL_GetError());
<< ": " << SDL_GetError() << '\n';
} else { } else {
std::cout << "GamepadManager: " << ADDED << " mappings carregats de " << db_path << '\n'; Log::info("GamepadManager", "{} mappings carregats de {}", ADDED, db_path);
} }
int count = 0; int count = 0;
@@ -175,7 +171,7 @@ void GamepadManager::init() {
} }
SDL_free(ids); SDL_free(ids);
} }
std::cout << "GamepadManager: " << entries_.size() << " mando(s) detectat(s) a l'inici\n"; Log::info("GamepadManager", "{} mando(s) detectat(s) a l'inici", entries_.size());
initialized_ = true; initialized_ = true;
} }
@@ -268,8 +264,7 @@ void GamepadManager::assignToPlayer(int player_id, const std::string& name, cons
if (a.pad != nullptr) { if (a.pad != nullptr) {
for (auto& other : assignments_) { for (auto& other : assignments_) {
if (&other != &a && other.pad == a.pad) { if (&other != &a && other.pad == a.pad) {
std::cout << "GamepadManager: mando " << a.name Log::info("GamepadManager", "mando {} reassignat (last-wins)", a.name);
<< " reassignat (last-wins)\n";
other.pad = nullptr; other.pad = nullptr;
} }
} }
@@ -277,11 +272,9 @@ void GamepadManager::assignToPlayer(int player_id, const std::string& name, cons
// Traç de l'assignació persistida (vinculada o pendent) perquè quede // Traç de l'assignació persistida (vinculada o pendent) perquè quede
// visible a la consola igual que les autoassignacions. // visible a la consola igual que les autoassignacions.
if (a.pad != nullptr) { if (a.pad != nullptr) {
std::cout << "GamepadManager: assignat " << a.name Log::info("GamepadManager", "assignat {} → P{}", a.name, player_id + 1);
<< " → P" << (player_id + 1) << '\n';
} else { } else {
std::cout << "GamepadManager: P" << (player_id + 1) << " pendent (" Log::info("GamepadManager", "P{} pendent ({} no connectat)", player_id + 1, a.name.empty() ? a.path : a.name);
<< (a.name.empty() ? a.path : a.name) << " no connectat)\n";
} }
} }
@@ -307,8 +300,7 @@ auto GamepadManager::autoAssignFreeSlots() -> std::vector<AutoAssigned> {
slot.name = e.pretty_name; slot.name = e.pretty_name;
slot.path = e.path; slot.path = e.path;
assigned.push_back(AutoAssigned{.player_id = pid, .name = e.pretty_name, .path = e.path}); assigned.push_back(AutoAssigned{.player_id = pid, .name = e.pretty_name, .path = e.path});
std::cout << "GamepadManager: auto-assigned " << e.pretty_name Log::info("GamepadManager", "auto-assigned {} → P{}", e.pretty_name, pid + 1);
<< " → P" << (pid + 1) << '\n';
break; break;
} }
} }
+2 -2
View File
@@ -26,7 +26,7 @@ namespace GlobalInputs {
// La lliguem a la durada de la notificació EXIT perquè el missatge i // La lliguem a la durada de la notificació EXIT perquè el missatge i
// la confirmació expiren alhora — si no, l'usuari podria veure el // la confirmació expiren alhora — si no, l'usuari podria veure el
// text però ja no ser dins del rang de confirmació (o al revés). // text però ja no ser dins del rang de confirmació (o al revés).
// El valor concret viu a data/ui.yaml (timings.exit_ms) via UiConfig. // El valor concret viu a data/config/ui.yaml (timings.exit_ms) via UiConfig.
constexpr int MIN_ZOOM = 1; constexpr int MIN_ZOOM = 1;
constexpr int MAX_ZOOM = 6; constexpr int MAX_ZOOM = 6;
@@ -161,7 +161,7 @@ namespace GlobalInputs {
// Debug helper: dispara una notificació prou llarga perquè // Debug helper: dispara una notificació prou llarga perquè
// `wrapToWidth` la parteixi en (almenys) dues línies. Útil per a // `wrapToWidth` la parteixi en (almenys) dues línies. Útil per a
// verificar visualment el padding asimètric, el line_gap i la caixa // verificar visualment el padding asimètric, el line_gap i la caixa
// multilinia després de tocar la geometria a `data/ui.yaml`. // multilinia després de tocar la geometria a `data/config/ui.yaml`.
void onDebugLongNotification() { void onDebugLongNotification() {
Notification::notify("Notificació de prova llarga per a comprovar que el text es parteix correctament en dues línies dins la caixa."); Notification::notify("Notificació de prova llarga per a comprovar que el text es parteix correctament en dues línies dins la caixa.");
} }
+6 -6
View File
@@ -1,11 +1,11 @@
#include "core/input/key_config.hpp" #include "core/input/key_config.hpp"
#include <fstream> #include <fstream>
#include <iostream>
#include <string> #include <string>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
auto KeyConfig::get() -> KeyConfig& { auto KeyConfig::get() -> KeyConfig& {
static KeyConfig instance_; static KeyConfig instance_;
@@ -15,7 +15,7 @@ auto KeyConfig::get() -> KeyConfig& {
void KeyConfig::load(const std::string& file_path) { void KeyConfig::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path); auto bytes = Resource::Helper::loadFile(file_path);
if (bytes.empty()) { if (bytes.empty()) {
std::cerr << "KeyConfig: no s'ha pogut obrir " << file_path << '\n'; Log::error("KeyConfig", "no s'ha pogut obrir {}", file_path);
return; return;
} }
@@ -24,7 +24,7 @@ void KeyConfig::load(const std::string& file_path) {
const auto* end = begin + bytes.size(); const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end); auto yaml = fkyaml::node::deserialize(begin, end);
if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) { if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) {
std::cerr << "KeyConfig: falta la secció 'keys' a " << file_path << '\n'; Log::error("KeyConfig", "falta la secció 'keys' a {}", file_path);
return; return;
} }
bindings_.clear(); bindings_.clear();
@@ -34,14 +34,14 @@ void KeyConfig::load(const std::string& file_path) {
const auto CODE = entry["code"].get_value<std::string>(); const auto CODE = entry["code"].get_value<std::string>();
const SDL_Scancode SC = SDL_GetScancodeFromName(CODE.c_str()); const SDL_Scancode SC = SDL_GetScancodeFromName(CODE.c_str());
if (SC == SDL_SCANCODE_UNKNOWN) { if (SC == SDL_SCANCODE_UNKNOWN) {
std::cerr << "KeyConfig: codi SDL desconegut '" << CODE << "' per id '" << ID << "'\n"; Log::warn("KeyConfig", "codi SDL desconegut '{}' per id '{}'", CODE, ID);
continue; continue;
} }
bindings_[ID] = SC; bindings_[ID] = SC;
} }
std::cout << "KeyConfig: " << bindings_.size() << " bindings des de " << file_path << '\n'; Log::info("KeyConfig", "{} bindings des de {}", bindings_.size(), file_path);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "KeyConfig: error parsejant " << file_path << ": " << e.what() << '\n'; Log::error("KeyConfig", "error parsejant {}: {}", file_path, e.what());
} }
} }
+1 -1
View File
@@ -5,7 +5,7 @@
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
// Mapeig id → SDL_Scancode carregat des de data/input/keys.yaml. Consumit // Mapeig id → SDL_Scancode carregat des de data/config/input.yaml. Consumit
// per GlobalInputs i (en el futur) per menús de remapat. No té cap cost // per GlobalInputs i (en el futur) per menús de remapat. No té cap cost
// global: si la clau no existeix, retorna SDL_SCANCODE_UNKNOWN i GlobalInputs // global: si la clau no existeix, retorna SDL_SCANCODE_UNKNOWN i GlobalInputs
// ignora l'acció. D'aquesta manera podem afegir nous bindings al YAML sense // ignora l'acció. D'aquesta manera podem afegir nous bindings al YAML sense
+5 -5
View File
@@ -1,11 +1,11 @@
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include <fstream> #include <fstream>
#include <iostream>
#include <string> #include <string>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -36,7 +36,7 @@ auto Locale::get() -> Locale& {
void Locale::load(const std::string& file_path) { void Locale::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path); auto bytes = Resource::Helper::loadFile(file_path);
if (bytes.empty()) { if (bytes.empty()) {
std::cerr << "Locale: no s'ha pogut obrir " << file_path << '\n'; Log::error("Locale", "no s'ha pogut obrir {}", file_path);
return; return;
} }
@@ -46,15 +46,15 @@ void Locale::load(const std::string& file_path) {
auto yaml = fkyaml::node::deserialize(begin, end); auto yaml = fkyaml::node::deserialize(begin, end);
strings_.clear(); strings_.clear();
flatten(yaml, "", strings_); flatten(yaml, "", strings_);
std::cout << "Locale: " << strings_.size() << " traduccions des de " << file_path << '\n'; Log::info("Locale", "{} traduccions des de {}", strings_.size(), file_path);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "Locale: error parsejant " << file_path << ": " << e.what() << '\n'; Log::error("Locale", "error parsejant {}: {}", file_path, e.what());
} }
} }
auto Locale::text(const std::string& key) const -> std::string { auto Locale::text(const std::string& key) const -> std::string {
auto it = strings_.find(key); auto it = strings_.find(key);
if (it != strings_.end()) { return it->second; } if (it != strings_.end()) { return it->second; }
std::cerr << "Locale: clau no trobada: " << key << '\n'; Log::warn("Locale", "clau no trobada: {}", key);
return key; return key;
} }
@@ -0,0 +1,219 @@
#include "core/rendering/effects/scan_reveal.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <numbers>
#include <random>
#include "core/rendering/screen.hpp"
#include "core/rendering/surface.hpp"
namespace Effects {
namespace {
// Hash determinista per a (row, time_bucket): retorna float a
// [-1, 1]. Usat per generar el jitter horitzontal de la banda
// sense necessitat d'un RNG d'estat (cada frame el resultat
// canvia perquè el time_bucket canvia, però dins d'un mateix
// bucket dues files diferents donen valors distints).
auto rowJitter(int row, int time_bucket) -> float {
const auto SEED = static_cast<std::uint32_t>((row * 73856093) ^ (time_bucket * 19349663));
// Mix barato (xorshift) per descorrelacionar bits.
std::uint32_t x = SEED;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
// Normalitzem a [-1, 1].
const float NORM = static_cast<float>(x) / static_cast<float>(UINT32_MAX);
return (NORM * 2.0F) - 1.0F;
}
// Bucket de temps per al jitter: agrupant en blocs de ~50ms el
// jitter "salta" 20 cops per segon — visualment "viu" sense
// ser caos pur a 60fps.
constexpr int JITTER_TIME_BUCKET_MS = 50;
} // namespace
ScanReveal::ScanReveal()
: rng_(std::random_device{}()) {}
void ScanReveal::begin(const Params& params) {
params_ = params;
elapsed_ms_ = 0;
flash_remaining_ms_ = 0;
flash_started_ = false;
burst_remaining_ms_ = 0;
burst_y_norm_ = 0.0F;
burst_dx_ = 0;
tail_started_ = false;
tail_remaining_ms_ = 0;
}
void ScanReveal::tick(int delta_ms) {
if (params_.duration_ms <= 0) { return; }
// 1) Avanç del scan (clampejat a duration_ms).
elapsed_ms_ = std::min(elapsed_ms_ + delta_ms, params_.duration_ms);
// 2) Tick del burst VHS actiu, si n'hi ha.
if (burst_remaining_ms_ > 0) {
burst_remaining_ms_ = std::max(0, burst_remaining_ms_ - delta_ms);
}
// 3) Spawn d'un nou burst. Permès durant el scan i durant el
// glitch tail post-flash; bloquejat exactament durant el
// flash perquè aquell és un punt visual de tancament net.
// Probabilitat per frame ≈ (per_sec/1000) · delta_ms.
const bool TAIL_ACTIVE = tail_started_ && tail_remaining_ms_ > 0;
const bool SCAN_ONGOING = elapsed_ms_ < params_.duration_ms;
if (burst_remaining_ms_ == 0 && (SCAN_ONGOING || TAIL_ACTIVE) && params_.vhs_burst_per_sec > 0 && params_.vhs_burst_duration_ms > 0) {
const float P = (static_cast<float>(params_.vhs_burst_per_sec) / 1000.0F) * static_cast<float>(delta_ms);
std::uniform_real_distribution<float> dist01(0.0F, 1.0F);
if (dist01(rng_) < P) {
burst_remaining_ms_ = params_.vhs_burst_duration_ms;
burst_y_norm_ = dist01(rng_);
std::uniform_int_distribution<int> dist_sign(0, 1);
const int SIGN = (dist_sign(rng_) == 0) ? 1 : -1;
burst_dx_ = SIGN * params_.vhs_burst_amplitude_px;
}
}
// 4) Dispara el flash quan el scan acaba (una sola vegada).
if (!flash_started_ && elapsed_ms_ >= params_.duration_ms && params_.flash_ms > 0) {
flash_started_ = true;
flash_remaining_ms_ = params_.flash_ms;
}
// Decrement del flash mentre estiga actiu.
if (flash_remaining_ms_ > 0) {
flash_remaining_ms_ = std::max(0, flash_remaining_ms_ - delta_ms);
}
// 5) Glitch tail: arranca un sol cop quan el scan ha acabat i el
// flash s'ha esgotat. Si no hi havia flash configurat, també
// arranca tot just acabar el scan.
if (!tail_started_ && elapsed_ms_ >= params_.duration_ms && flash_remaining_ms_ <= 0 && params_.tail_glitch_ms > 0) {
tail_started_ = true;
tail_remaining_ms_ = params_.tail_glitch_ms;
}
if (tail_remaining_ms_ > 0) {
tail_remaining_ms_ = std::max(0, tail_remaining_ms_ - delta_ms);
}
}
auto ScanReveal::isActive() const -> bool {
return params_.duration_ms > 0 && !isFinished();
}
auto ScanReveal::isFinished() const -> bool {
if (params_.duration_ms <= 0) { return true; }
// Acabat = scan al final + flash exhaurit + glitch tail
// exhaurit (o sense tail configurat).
const bool SCAN_DONE = elapsed_ms_ >= params_.duration_ms;
const bool FLASH_DONE = flash_remaining_ms_ <= 0;
const bool TAIL_DONE = (params_.tail_glitch_ms <= 0) || (tail_started_ && tail_remaining_ms_ <= 0);
return SCAN_DONE && FLASH_DONE && TAIL_DONE;
}
void ScanReveal::render(const Surface& src, int x, int y) const {
if (params_.duration_ms <= 0) {
// Sense efecte: render pla.
src.render(x, y);
return;
}
const int H = src.getHeight();
const auto W_F = static_cast<float>(src.getWidth());
const float T = static_cast<float>(elapsed_ms_) / static_cast<float>(params_.duration_ms);
// Banda: comença a y=-band_height i acaba a y=H. Així al t=0 cap
// fila és visible i al t=1 totes ho són.
const float SWEEP_F = -static_cast<float>(params_.band_height_px) + (T * static_cast<float>(H + params_.band_height_px));
const int BAND_TOP = static_cast<int>(std::floor(SWEEP_F));
const int BAND_BOT = BAND_TOP + params_.band_height_px;
const int TIME_BUCKET = elapsed_ms_ / JITTER_TIME_BUCKET_MS;
const int BURST_Y_PX = static_cast<int>(burst_y_norm_ * static_cast<float>(H));
const bool BURST_ACTIVE = burst_remaining_ms_ > 0;
// Durant el glitch tail post-flash, BAND_TOP queda just damunt
// del peu del logo, fet que (amb el codi de jitter que segueix)
// generaria jitter residual a les últimes files. Aquí el
// suprimim explícitament: el tail només viu de bursts VHS.
const bool TAIL_ACTIVE = tail_started_ && tail_remaining_ms_ > 0;
// Render fila a fila (només files revelades).
for (int row = 0; row <= BAND_BOT && row < H; ++row) {
int dx = 0;
// Jitter de banda: amplitud màxima a la banda, falloff
// lineal cap als marges del radi de glitch. Inactiu durant
// el glitch tail (no hi ha banda).
const int DIST_TO_BAND = std::abs(row - BAND_TOP);
if (!TAIL_ACTIVE && params_.glitch_band_radius_px > 0 && DIST_TO_BAND <= params_.glitch_band_radius_px && params_.glitch_amplitude_px > 0) {
const float FALLOFF = 1.0F - (static_cast<float>(DIST_TO_BAND) / static_cast<float>(params_.glitch_band_radius_px));
const float JIT = rowJitter(row, TIME_BUCKET);
dx += static_cast<int>(std::lround(
FALLOFF * static_cast<float>(params_.glitch_amplitude_px) * JIT));
}
// Burst VHS: només damunt la part ja revelada (row < BAND_TOP)
// perquè per sota encara no hi ha imatge "estable" que poder
// distorsionar visualment.
if (BURST_ACTIVE && row < BAND_TOP && row >= BURST_Y_PX && row < BURST_Y_PX + params_.vhs_burst_height_px) {
dx += burst_dx_;
}
SDL_FRect row_rect{0.0F, static_cast<float>(row), W_F, 1.0F};
src.render(x + dx, y + row, &row_rect);
}
auto& screen = Screen::get();
// Capa lluminosa de la banda. Verticalment només cobreix les
// files del logo (és on viu el scan), però horitzontalment
// travessa tot el canvas com un feix CRT, així sobre fons negre
// no es veuen les costures del rectangle del Surface. L'alpha
// base es modula amb un pulse sinusoidal a `band_pulse_hz` per
// simular el ritme de refresc d'un CRT.
if (params_.band_alpha > 0 && params_.band_height_px > 0) {
const int VISIBLE_TOP = std::clamp(BAND_TOP, 0, H);
const int VISIBLE_BOT = std::clamp(BAND_BOT, 0, H);
const int VISIBLE_H = VISIBLE_BOT - VISIBLE_TOP;
if (VISIBLE_H > 0) {
// Pulse: fracció ∈ [0,1] avançada a la freqüència donada,
// mapejada a un multiplicador ∈ [1-depth, 1] sobre l'alpha
// base. Amb depth=0 el pulse és inactiu (multiplicador 1).
const float PULSE_DEPTH = std::clamp(params_.band_pulse_depth, 0.0F, 1.0F);
const float TWO_PI = 2.0F * std::numbers::pi_v<float>;
const float PHASE = TWO_PI * params_.band_pulse_hz * (static_cast<float>(elapsed_ms_) / 1000.0F);
const float WAVE = 0.5F + (0.5F * std::sin(PHASE)); // 0..1
const float MULT = (1.0F - PULSE_DEPTH) + (PULSE_DEPTH * WAVE);
const float ALPHA_F = static_cast<float>(params_.band_alpha) * MULT;
const auto ALPHA = static_cast<std::uint8_t>(
std::clamp(static_cast<int>(std::round(ALPHA_F)), 0, 255));
if (ALPHA > 0) {
screen.blendRect(0, y + VISIBLE_TOP, screen.getCanvasWidth(), VISIBLE_H, 0xFFFFFFFFU, ALPHA);
}
}
}
// Flash final: capa blanca damunt de TOT el canvas (no només
// del rectangle del logo) — així el flash banya l'escena
// sencera com un CRT i no es veuen les costures del Surface
// sobre fons negre. Alpha proporcional a `flash_remaining_ms_
// / flash_ms` (decreix suau cap a 0 al final del flash).
if (flash_remaining_ms_ > 0 && params_.flash_ms > 0) {
const float FLASH_T = static_cast<float>(flash_remaining_ms_) / static_cast<float>(params_.flash_ms);
const auto ALPHA = static_cast<std::uint8_t>(
std::round(255.0F * std::clamp(FLASH_T, 0.0F, 1.0F)));
if (ALPHA > 0) {
screen.blendRect(0, 0, screen.getCanvasWidth(), screen.getCanvasHeight(), 0xFFFFFFFFU, ALPHA);
}
}
}
} // namespace Effects
@@ -0,0 +1,96 @@
#pragma once
#include <cstdint>
#include <random>
class Surface;
namespace Effects {
// Revelat tipus "scanline CRT" d'una Surface. Una banda lluminosa
// baixa des de dalt cap a baix de la imatge: les files damunt de la
// banda es renderitzen normalment (ja revelades), les files sota
// queden invisibles, i a la banda mateixa s'aplica un overlay
// blanc semi-transparent. Al voltant de la banda les files reben un
// jitter horitzontal aleatori (falloff lineal amb la distància) i
// damunt la part ja revelada apareixen ocasionalment "bursts VHS" —
// franges horitzontals desplaçades durant uns ms — per a un toc
// demoscene/VHS deteriorat.
//
// Quan la banda surt per sota del logo i s'esgota `duration_ms`,
// dispara un flash blanc breu. Si `tail_glitch_ms > 0`, després del
// flash el logo queda totalment visible mentre els bursts VHS
// continuen saltant durant aquest temps (glitch tail). Quan el tail
// s'exhaureix `isFinished()=true`. El caller decideix què fa amb el
// control de fase a partir d'aquí.
//
// Ús:
// ScanReveal sr;
// sr.begin({.duration_ms = 1500, ...});
// // cada frame:
// sr.tick(delta_ms);
// sr.render(*logo, x, y);
// if (sr.isFinished()) { ... canvi de fase ... }
class ScanReveal {
public:
struct Params {
int duration_ms;
int band_height_px;
int band_alpha;
float band_pulse_hz;
float band_pulse_depth;
int glitch_amplitude_px;
int glitch_band_radius_px;
int vhs_burst_per_sec;
int vhs_burst_duration_ms;
int vhs_burst_height_px;
int vhs_burst_amplitude_px;
int flash_ms;
// Glitch tail post-flash: el logo queda visible sencer i els
// bursts VHS continuen saltant durant aquest temps. 0 ho
// desactiva (efecte acaba al flash).
int tail_glitch_ms;
};
ScanReveal();
void begin(const Params& params);
void tick(int delta_ms);
// Renderitza un frame del revelat sobre la Surface `src`. El
// caller s'encarrega de pintar negre per sota (les files no
// revelades simplement no es dibuixen — queda el que hi haja al
// canvas, normalment negre).
void render(const Surface& src, int x, int y) const;
[[nodiscard]] auto isActive() const -> bool;
[[nodiscard]] auto isFinished() const -> bool;
private:
Params params_{};
int elapsed_ms_{0};
// Flash post-scan: arranca quan el scan toca fons; decreix amb
// `delta_ms` fins a 0. Mentre `flash_active_` és true encara no
// hem reportat finished (donant marge per pintar el flash sencer).
int flash_remaining_ms_{0};
bool flash_started_{false};
// Burst VHS actiu (com a màxim un alhora — simplifica i ja és
// suficient per al ritme visual). y_norm ∈ [0,1] es resol en
// píxels al `render()` perquè aquí no coneixem l'alçada del logo.
int burst_remaining_ms_{0};
float burst_y_norm_{0.0F};
int burst_dx_{0};
// Glitch tail post-flash: arranca quan el flash s'esgota i
// permet que els bursts continuen saltant amb el logo ja del tot
// visible. `isFinished()` espera al final d'aquest tail.
bool tail_started_{false};
int tail_remaining_ms_{0};
// RNG per als bursts. Seed des de random_device a `begin()`
// perquè cada execució sigui visualment diferent.
std::minstd_rand rng_;
};
} // namespace Effects
+6 -7
View File
@@ -1,10 +1,11 @@
#include "core/rendering/gif.hpp" #include "core/rendering/gif.hpp"
#include <cstring> #include <cstring>
#include <iostream>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include "utils/log.hpp"
namespace GIF { namespace GIF {
namespace { namespace {
@@ -111,7 +112,7 @@ namespace GIF {
while (cur_code != -1) { while (cur_code != -1) {
out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte; out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte;
if (dictionary[cur_code].prev == cur_code) { if (dictionary[cur_code].prev == cur_code) {
std::cerr << "Internal error; self-reference detected." << '\n'; Log::error("GIF", "internal error; self-reference detected.");
throw std::runtime_error("Internal error in decompress: self-reference"); throw std::runtime_error("Internal error in decompress: self-reference");
} }
cur_code = dictionary[cur_code].prev; cur_code = dictionary[cur_code].prev;
@@ -155,8 +156,7 @@ namespace GIF {
if (prev > -1 && code_length < LZW_MAX_CODE_LENGTH) { if (prev > -1 && code_length < LZW_MAX_CODE_LENGTH) {
if (code > dictionary_ind) { if (code > dictionary_ind) {
std::cerr << "code = " << std::hex << code Log::error("GIF", "code = {:x}, but dictionary_ind = {}", code, dictionary_ind);
<< ", but dictionary_ind = " << dictionary_ind << '\n';
throw std::runtime_error("LZW error: code exceeds dictionary_ind."); throw std::runtime_error("LZW error: code exceeds dictionary_ind.");
} }
@@ -167,8 +167,7 @@ namespace GIF {
// Verifica que 'code' sea un índice válido antes de usarlo. // Verifica que 'code' sea un índice válido antes de usarlo.
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) { if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
std::cerr << "Invalid LZW code " << code Log::error("GIF", "invalid LZW code {}, dictionary size {}", code, dictionary.size());
<< ", dictionary size " << dictionary.size() << '\n';
throw std::runtime_error("LZW error: invalid code encountered"); throw std::runtime_error("LZW error: invalid code encountered");
} }
@@ -287,7 +286,7 @@ namespace GIF {
} else if (block_type == IMAGE_DESCRIPTOR) { } else if (block_type == IMAGE_DESCRIPTOR) {
return processImageDescriptor(buffer); return processImageDescriptor(buffer);
} else { } else {
std::cerr << "Unrecognized block type " << std::hex << static_cast<int>(block_type) << '\n'; Log::error("GIF", "unrecognized block type {:x}", static_cast<int>(block_type));
return std::vector<uint8_t>{}; return std::vector<uint8_t>{};
} }
block_type = *buffer++; block_type = *buffer++;
@@ -9,7 +9,7 @@
// Temes per a les notificacions de pantalla. Un "preset" captura els dos // Temes per a les notificacions de pantalla. Un "preset" captura els dos
// colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat // colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat
// en mil·lisegons. Els valors concrets viuen a data/ui.yaml (carregat per // en mil·lisegons. Els valors concrets viuen a data/config/ui.yaml (carregat per
// UiConfig); per llançar una notificació amb un preset concret, cridar // UiConfig); per llançar una notificació amb un preset concret, cridar
// Notification::notify(text, preset). Sense preset s'usa el DEFAULT. // Notification::notify(text, preset). Sense preset s'usa el DEFAULT.
namespace Notification { namespace Notification {
@@ -22,7 +22,7 @@ namespace Notification {
// Preset per defecte: text clar sobre fons fosc. Es construeix en cada // Preset per defecte: text clar sobre fons fosc. Es construeix en cada
// crida a partir d'UiConfig perquè els valors no són constexpr: viuen a // crida a partir d'UiConfig perquè els valors no són constexpr: viuen a
// data/ui.yaml i es carreguen a l'arrencada. // data/config/ui.yaml i es carreguen a l'arrencada.
[[nodiscard]] inline auto defaultPreset() -> Preset { [[nodiscard]] inline auto defaultPreset() -> Preset {
return { return {
.text_color = UiConfig::get().notificationText(), .text_color = UiConfig::get().notificationText(),
+3 -3
View File
@@ -6,13 +6,13 @@
#include <cstddef> #include <cstddef>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <iostream>
#include <memory> #include <memory>
#include <ranges> #include <ranges>
#include "core/rendering/canvas.hpp" #include "core/rendering/canvas.hpp"
#include "core/rendering/sdl3gpu/viewport.hpp" #include "core/rendering/sdl3gpu/viewport.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -112,11 +112,11 @@ void Screen::createSdl() {
backend->setVSync(config_.vsync); backend->setVSync(config_.vsync);
if (backend->init(window_, canvas_width_, canvas_height_)) { if (backend->init(window_, canvas_width_, canvas_height_)) {
gpu_driver_ = backend->getDriverName(); gpu_driver_ = backend->getDriverName();
std::cout << "Screen: path GPU actiu (driver: " << gpu_driver_ << ")\n"; Log::info("Screen", "path GPU actiu (driver: {})", gpu_driver_);
shader_backend_ = std::move(backend); shader_backend_ = std::move(backend);
return; return;
} }
std::cerr << "Screen: GPU no disponible, fallback a SDL_Renderer\n"; Log::warn("Screen", "GPU no disponible, fallback a SDL_Renderer");
} }
gpu_driver_ = "sdl"; gpu_driver_ = "sdl";
@@ -1,10 +1,10 @@
#include "core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp" #include "core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp"
#include <cstring> #include <cstring>
#include <iostream>
#include "core/rendering/sdl3gpu/shader_factory.hpp" #include "core/rendering/sdl3gpu/shader_factory.hpp"
#include "core/rendering/sdl3gpu/viewport.hpp" #include "core/rendering/sdl3gpu/viewport.hpp"
#include "utils/log.hpp"
#ifdef __APPLE__ #ifdef __APPLE__
#include "core/rendering/sdl3gpu/msl/crtpi_frag.msl.h" #include "core/rendering/sdl3gpu/msl/crtpi_frag.msl.h"
@@ -29,7 +29,7 @@ namespace Rendering {
SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#endif #endif
if (vert == nullptr || frag == nullptr) { if (vert == nullptr || frag == nullptr) {
std::cerr << "CrtPiPipeline: creació de shaders fallida: " << SDL_GetError() << '\n'; Log::error("CrtPiPipeline", "creació de shaders fallida: {}", SDL_GetError());
if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); } if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); }
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); } if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
return false; return false;
@@ -57,7 +57,7 @@ namespace Rendering {
SDL_ReleaseGPUShader(device_, frag); SDL_ReleaseGPUShader(device_, frag);
if (pipeline_ == nullptr) { if (pipeline_ == nullptr) {
std::cerr << "CrtPiPipeline: creació del pipeline ha fallat: " << SDL_GetError() << '\n'; Log::error("CrtPiPipeline", "creació del pipeline ha fallat: {}", SDL_GetError());
return false; return false;
} }
@@ -2,10 +2,10 @@
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <iostream>
#include "core/rendering/sdl3gpu/shader_factory.hpp" #include "core/rendering/sdl3gpu/shader_factory.hpp"
#include "core/rendering/sdl3gpu/viewport.hpp" #include "core/rendering/sdl3gpu/viewport.hpp"
#include "utils/log.hpp"
#ifdef __APPLE__ #ifdef __APPLE__
#include "core/rendering/sdl3gpu/msl/postfx_frag.msl.h" #include "core/rendering/sdl3gpu/msl/postfx_frag.msl.h"
@@ -59,11 +59,11 @@ namespace Rendering {
const char* driver = preferred_driver_.empty() ? nullptr : preferred_driver_.c_str(); const char* driver = preferred_driver_.empty() ? nullptr : preferred_driver_.c_str();
device_ = SDL_CreateGPUDevice(FORMAT, false, driver); device_ = SDL_CreateGPUDevice(FORMAT, false, driver);
if (device_ == nullptr) { if (device_ == nullptr) {
std::cerr << "SDL3GPUShader: SDL_CreateGPUDevice ha fallat: " << SDL_GetError() << '\n'; Log::error("SDL3GPUShader", "SDL_CreateGPUDevice ha fallat: {}", SDL_GetError());
return false; return false;
} }
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) { if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
std::cerr << "SDL3GPUShader: SDL_ClaimWindowForGPUDevice ha fallat: " << SDL_GetError() << '\n'; Log::error("SDL3GPUShader", "SDL_ClaimWindowForGPUDevice ha fallat: {}", SDL_GetError());
return false; return false;
} }
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_)); SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
@@ -82,7 +82,7 @@ namespace Rendering {
fragment_shader_ = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); fragment_shader_ = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#endif #endif
if (vertex_shader_ == nullptr || fragment_shader_ == nullptr) { if (vertex_shader_ == nullptr || fragment_shader_ == nullptr) {
std::cerr << "SDL3GPUShader: SDL_CreateGPUShader ha fallat: " << SDL_GetError() << '\n'; Log::error("SDL3GPUShader", "SDL_CreateGPUShader ha fallat: {}", SDL_GetError());
return false; return false;
} }
return true; return true;
@@ -105,7 +105,7 @@ namespace Rendering {
pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &info); pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &info);
if (pipeline_ == nullptr) { if (pipeline_ == nullptr) {
std::cerr << "SDL3GPUShader: SDL_CreateGPUGraphicsPipeline ha fallat: " << SDL_GetError() << '\n'; Log::error("SDL3GPUShader", "SDL_CreateGPUGraphicsPipeline ha fallat: {}", SDL_GetError());
return false; return false;
} }
return true; return true;
@@ -137,7 +137,7 @@ namespace Rendering {
sampler_ = SDL_CreateGPUSampler(device_, &samp_info); sampler_ = SDL_CreateGPUSampler(device_, &samp_info);
if (scene_texture_ == nullptr || upload_buffer_ == nullptr || sampler_ == nullptr) { if (scene_texture_ == nullptr || upload_buffer_ == nullptr || sampler_ == nullptr) {
std::cerr << "SDL3GPUShader: creació de recursos GPU ha fallat: " << SDL_GetError() << '\n'; Log::error("SDL3GPUShader", "creació de recursos GPU ha fallat: {}", SDL_GetError());
return false; return false;
} }
return true; return true;
@@ -1,12 +1,12 @@
#include "core/rendering/sprite/animated_sprite.hpp" #include "core/rendering/sprite/animated_sprite.hpp"
#include <cmath> #include <cmath>
#include <iostream>
#include <numeric> #include <numeric>
#include <utility> #include <utility>
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "utils/log.hpp"
// El parser pure de YAML d'animacions (parseAnimationsFromYaml + helpers) // El parser pure de YAML d'animacions (parseAnimationsFromYaml + helpers)
// viu a `animated_sprite_parser.cpp` perquè és testable sense Surface ni // viu a `animated_sprite_parser.cpp` perquè és testable sense Surface ni
@@ -74,10 +74,10 @@ AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
if (!first.frames.empty()) { setClip(first.frames[0].rect); } if (!first.frames.empty()) { setClip(first.frames[0].rect); }
} }
} catch (const fkyaml::exception& e) { } catch (const fkyaml::exception& e) {
std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n'; Log::error("AnimatedSprite", "YAML parsing error in animation {}: {}", cached_data.name, e.what());
throw; throw;
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "Error loading animation " << cached_data.name << ": " << e.what() << '\n'; Log::error("AnimatedSprite", "Error loading animation {}: {}", cached_data.name, e.what());
throw; throw;
} }
} }
@@ -140,7 +140,7 @@ void AnimatedSprite::setCurrentAnimation(const std::string& name) {
if (current_animation_ == name) { return; } if (current_animation_ == name) { return; }
auto it = animations_.find(name); auto it = animations_.find(name);
if (it == animations_.end()) { if (it == animations_.end()) {
std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n'; Log::warn("AnimatedSprite", "could not find \"{}\" animation", name);
return; return;
} }
current_animation_ = name; current_animation_ = name;
+1 -2
View File
@@ -2,7 +2,6 @@
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
#include <iostream>
#include <stdexcept> #include <stdexcept>
#include <vector> #include <vector>
@@ -63,7 +62,7 @@ Surface::Surface(const std::string& file_path) {
palette_[i] = Rendering::OPAQUE_ALPHA | rgb[i]; palette_[i] = Rendering::OPAQUE_ALPHA | rgb[i];
} }
printWithDots("Surface : ", getFileName(file_path), "[ LOADED ]"); printWithDots("Surface", getFileName(file_path), "[ LOADED ]");
} }
void Surface::clear(Uint8 color) { void Surface::clear(Uint8 color) {
+3 -3
View File
@@ -3,7 +3,6 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cstddef> #include <cstddef>
#include <iostream>
#include <sstream> #include <sstream>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
@@ -11,6 +10,7 @@
#include "core/rendering/sprite/sprite.hpp" #include "core/rendering/sprite/sprite.hpp"
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "utils/log.hpp"
#include "utils/utils.hpp" #include "utils/utils.hpp"
// Decodifica el pròxim codepoint UTF-8 de la cadena i avança 'pos' al byte // Decodifica el pròxim codepoint UTF-8 de la cadena i avança 'pos' al byte
@@ -55,7 +55,7 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
auto file_data = Resource::Helper::loadFile(file_path); auto file_data = Resource::Helper::loadFile(file_path);
if (file_data.empty()) { if (file_data.empty()) {
std::cerr << "Error: Fitxer no trobat " << getFileName(file_path) << '\n'; Log::error("Text File", "fitxer no trobat: {}", getFileName(file_path));
throw std::runtime_error("Fitxer no trobat: " + getFileName(file_path)); throw std::runtime_error("Fitxer no trobat: " + getFileName(file_path));
} }
@@ -101,7 +101,7 @@ auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
} }
} }
printWithDots("Text File : ", getFileName(file_path), "[ LOADED ]"); printWithDots("Text File", getFileName(file_path), "[ LOADED ]");
return tf; return tf;
} }
+10 -10
View File
@@ -1,12 +1,12 @@
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include <algorithm> #include <algorithm>
#include <iostream>
#include <stdexcept> #include <stdexcept>
#include "core/audio/jail_audio.hpp" #include "core/audio/jail_audio.hpp"
#include "core/resources/palette_loader.hpp" #include "core/resources/palette_loader.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "utils/log.hpp"
#include "utils/utils.hpp" #include "utils/utils.hpp"
// Deleters dels recursos d'àudio: necessiten veure el tipus complet de // Deleters dels recursos d'àudio: necessiten veure el tipus complet de
@@ -37,19 +37,19 @@ namespace Resource {
throw std::runtime_error("Cache: cannot load animation " + file_path); throw std::runtime_error("Cache: cannot load animation " + file_path);
} }
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = std::move(bytes)}); animations_.emplace_back(AnimationResource{.name = name, .yaml_data = std::move(bytes)});
printWithDots("Animation : ", getFileName(file_path), "[ LOADED ]"); printWithDots("Animation", getFileName(file_path), "[ LOADED ]");
} }
void Cache::loadPalette(const std::string& name, const std::string& file_path) { void Cache::loadPalette(const std::string& name, const std::string& file_path) {
auto palette = PaletteLoader::loadFromFile(file_path); auto palette = PaletteLoader::loadFromFile(file_path);
palettes_.emplace_back(PaletteResource{.name = name, .palette = palette}); palettes_.emplace_back(PaletteResource{.name = name, .palette = palette});
printWithDots("Palette : ", getFileName(file_path), "[ LOADED ]"); printWithDots("Palette", getFileName(file_path), "[ LOADED ]");
} }
auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> { auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> {
auto it = std::ranges::find_if(surfaces_, [&name](const auto& s) { return s.name == name; }); auto it = std::ranges::find_if(surfaces_, [&name](const auto& s) { return s.name == name; });
if (it == surfaces_.end()) { if (it == surfaces_.end()) {
std::cerr << "Cache: surface not found: " << name << '\n'; Log::error("Cache", "surface not found: {}", name);
throw std::runtime_error("Surface not found: " + name); throw std::runtime_error("Surface not found: " + name);
} }
return it->surface; return it->surface;
@@ -58,7 +58,7 @@ namespace Resource {
auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& { auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& {
auto it = std::ranges::find_if(animations_, [&name](const auto& a) { return a.name == name; }); auto it = std::ranges::find_if(animations_, [&name](const auto& a) { return a.name == name; });
if (it == animations_.end()) { if (it == animations_.end()) {
std::cerr << "Cache: animation not found: " << name << '\n'; Log::error("Cache", "animation not found: {}", name);
throw std::runtime_error("Animation not found: " + name); throw std::runtime_error("Animation not found: " + name);
} }
return *it; return *it;
@@ -67,7 +67,7 @@ namespace Resource {
auto Cache::getPalette(const std::string& name) -> const Palette& { auto Cache::getPalette(const std::string& name) -> const Palette& {
auto it = std::ranges::find_if(palettes_, [&name](const auto& p) { return p.name == name; }); auto it = std::ranges::find_if(palettes_, [&name](const auto& p) { return p.name == name; });
if (it == palettes_.end()) { if (it == palettes_.end()) {
std::cerr << "Cache: palette not found: " << name << '\n'; Log::error("Cache", "palette not found: {}", name);
throw std::runtime_error("Palette not found: " + name); throw std::runtime_error("Palette not found: " + name);
} }
return it->palette; return it->palette;
@@ -83,7 +83,7 @@ namespace Resource {
throw std::runtime_error("Cache: Ja::loadMusic failed for " + file_path); throw std::runtime_error("Cache: Ja::loadMusic failed for " + file_path);
} }
musics_.emplace_back(MusicResource{.name = name, .music = std::unique_ptr<Ja::Music, MusicDeleter>(raw)}); musics_.emplace_back(MusicResource{.name = name, .music = std::unique_ptr<Ja::Music, MusicDeleter>(raw)});
printWithDots("Music : ", getFileName(file_path), "[ LOADED ]"); printWithDots("Music", getFileName(file_path), "[ LOADED ]");
} }
void Cache::loadSound(const std::string& name, const std::string& file_path) { void Cache::loadSound(const std::string& name, const std::string& file_path) {
@@ -96,13 +96,13 @@ namespace Resource {
throw std::runtime_error("Cache: Ja::loadSound failed for " + file_path); throw std::runtime_error("Cache: Ja::loadSound failed for " + file_path);
} }
sounds_.emplace_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(raw)}); sounds_.emplace_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(raw)});
printWithDots("Sound : ", getFileName(file_path), "[ LOADED ]"); printWithDots("Sound", getFileName(file_path), "[ LOADED ]");
} }
auto Cache::getMusic(const std::string& name) -> Ja::Music* { auto Cache::getMusic(const std::string& name) -> Ja::Music* {
auto it = std::ranges::find_if(musics_, [&name](const auto& m) { return m.name == name; }); auto it = std::ranges::find_if(musics_, [&name](const auto& m) { return m.name == name; });
if (it == musics_.end()) { if (it == musics_.end()) {
std::cerr << "Cache: music not found: " << name << '\n'; Log::error("Cache", "music not found: {}", name);
throw std::runtime_error("Music not found: " + name); throw std::runtime_error("Music not found: " + name);
} }
return it->music.get(); return it->music.get();
@@ -111,7 +111,7 @@ namespace Resource {
auto Cache::getSound(const std::string& name) -> Ja::Sound* { auto Cache::getSound(const std::string& name) -> Ja::Sound* {
auto it = std::ranges::find_if(sounds_, [&name](const auto& s) { return s.name == name; }); auto it = std::ranges::find_if(sounds_, [&name](const auto& s) { return s.name == name; });
if (it == sounds_.end()) { if (it == sounds_.end()) {
std::cerr << "Cache: sound not found: " << name << '\n'; Log::error("Cache", "sound not found: {}", name);
throw std::runtime_error("Sound not found: " + name); throw std::runtime_error("Sound not found: " + name);
} }
return it->sound.get(); return it->sound.get();
+8 -10
View File
@@ -1,11 +1,12 @@
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include <fstream> #include <fstream>
#include <iostream>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include "core/resources/resource_pack.hpp" #include "core/resources/resource_pack.hpp"
#include "utils/defaults.hpp"
#include "utils/log.hpp"
namespace Resource::Helper { namespace Resource::Helper {
@@ -26,14 +27,14 @@ namespace Resource::Helper {
// Silenciem si veníem de prefer_disk: aleshores caurem // Silenciem si veníem de prefer_disk: aleshores caurem
// al pack i no és cap error real que el fitxer no estigui // al pack i no és cap error real que el fitxer no estigui
// al disc. // al disc.
if (verbose) { std::cerr << "Resource::Helper: cannot open " << FULL << '\n'; } if (verbose) { Log::error("Resource::Helper", "cannot open {}", FULL); }
return {}; return {};
} }
std::streamsize size = file.tellg(); std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg); file.seekg(0, std::ios::beg);
std::vector<std::uint8_t> data(static_cast<std::size_t>(size)); std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
if (!file.read(reinterpret_cast<char*>(data.data()), size)) { if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
std::cerr << "Resource::Helper: failed to read " << FULL << '\n'; Log::error("Resource::Helper", "failed to read {}", FULL);
return {}; return {};
} }
return data; return data;
@@ -51,14 +52,11 @@ namespace Resource::Helper {
g_pack_loaded = g_pack.loadPack(pack_file); g_pack_loaded = g_pack.loadPack(pack_file);
if (g_pack_loaded) { if (g_pack_loaded) {
std::cout << "Resource::Helper: pack loaded (" << g_pack.getResourceCount() Log::info("Resource::Helper", "pack loaded ({} entries) from {}", g_pack.getResourceCount(), pack_file);
<< " entries) from " << pack_file << '\n';
} else if (enable_fallback) { } else if (enable_fallback) {
std::cout << "Resource::Helper: no pack at " << pack_file Log::info("Resource::Helper", "no pack at {} — using filesystem fallback", pack_file);
<< " — using filesystem fallback\n";
} else { } else {
std::cerr << "Resource::Helper: FATAL — no pack at " << pack_file Log::critical("Resource::Helper", "FATAL — no pack at {} and fallback disabled", pack_file);
<< " and fallback disabled\n";
return false; return false;
} }
return true; return true;
@@ -92,7 +90,7 @@ namespace Resource::Helper {
// carpeta `data/`). // carpeta `data/`).
const auto FROM_PACK = [&filepath]() -> std::vector<std::uint8_t> { const auto FROM_PACK = [&filepath]() -> std::vector<std::uint8_t> {
if (!g_pack_loaded) { return {}; } if (!g_pack_loaded) { return {}; }
constexpr std::string_view PREFIX = "data/"; const std::string_view PREFIX{Defaults::Paths::DATA_ROOT};
std::string key = filepath; std::string key = filepath;
if (key.starts_with(PREFIX)) { key.erase(0, PREFIX.size()); } if (key.starts_with(PREFIX)) { key.erase(0, PREFIX.size()); }
if (g_pack.hasResource(key)) { return g_pack.getResource(key); } if (g_pack.hasResource(key)) { return g_pack.getResource(key); }
+11 -10
View File
@@ -4,11 +4,12 @@
#include <array> #include <array>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream>
#include "utils/log.hpp"
namespace Resource { namespace Resource {
const std::string Pack::DEFAULT_ENCRYPT_KEY = "AEE_2026_PACK_KEY"; const std::string Pack::DEFAULT_ENCRYPT_KEY = "AEE_ARCADE_PACK_KEY";
namespace { namespace {
constexpr const char* MAGIC = "AE26"; constexpr const char* MAGIC = "AE26";
@@ -40,14 +41,14 @@ namespace Resource {
std::array<char, 4> header{}; std::array<char, 4> header{};
file.read(header.data(), 4); file.read(header.data(), 4);
if (std::string(header.data(), 4) != MAGIC) { if (std::string(header.data(), 4) != MAGIC) {
std::cerr << "Resource::Pack: invalid pack file format (bad magic): " << pack_file << '\n'; Log::error("Resource::Pack", "invalid pack file format (bad magic): {}", pack_file);
return false; return false;
} }
std::uint32_t version = 0; std::uint32_t version = 0;
file.read(reinterpret_cast<char*>(&version), sizeof(version)); file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != VERSION) { if (version != VERSION) {
std::cerr << "Resource::Pack: unsupported pack version: " << version << '\n'; Log::error("Resource::Pack", "unsupported pack version: {}", version);
return false; return false;
} }
@@ -86,7 +87,7 @@ namespace Resource {
auto Pack::savePack(const std::string& pack_file) const -> bool { auto Pack::savePack(const std::string& pack_file) const -> bool {
std::ofstream file(pack_file, std::ios::binary); std::ofstream file(pack_file, std::ios::binary);
if (!file) { if (!file) {
std::cerr << "Resource::Pack: could not create pack file: " << pack_file << '\n'; Log::error("Resource::Pack", "could not create pack file: {}", pack_file);
return false; return false;
} }
@@ -120,7 +121,7 @@ namespace Resource {
auto Pack::addFile(const std::string& filename, const std::string& filepath) -> bool { auto Pack::addFile(const std::string& filename, const std::string& filepath) -> bool {
std::ifstream file(filepath, std::ios::binary | std::ios::ate); std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) { if (!file) {
std::cerr << "Resource::Pack: could not open file: " << filepath << '\n'; Log::error("Resource::Pack", "could not open file: {}", filepath);
return false; return false;
} }
@@ -129,7 +130,7 @@ namespace Resource {
std::vector<std::uint8_t> file_data(file_size); std::vector<std::uint8_t> file_data(file_size);
if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) { if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) {
std::cerr << "Resource::Pack: could not read file: " << filepath << '\n'; Log::error("Resource::Pack", "could not read file: {}", filepath);
return false; return false;
} }
@@ -146,7 +147,7 @@ namespace Resource {
auto Pack::addDirectory(const std::string& directory) -> bool { auto Pack::addDirectory(const std::string& directory) -> bool {
if (!std::filesystem::exists(directory)) { if (!std::filesystem::exists(directory)) {
std::cerr << "Resource::Pack: directory does not exist: " << directory << '\n'; Log::error("Resource::Pack", "directory does not exist: {}", directory);
return false; return false;
} }
@@ -169,7 +170,7 @@ namespace Resource {
const PackEntry& entry = it->second; const PackEntry& entry = it->second;
if (entry.offset + entry.size > data_.size()) { if (entry.offset + entry.size > data_.size()) {
std::cerr << "Resource::Pack: invalid resource data: " << filename << '\n'; Log::error("Resource::Pack", "invalid resource data: {}", filename);
return {}; return {};
} }
@@ -179,7 +180,7 @@ namespace Resource {
std::uint32_t checksum = calculateChecksum(result); std::uint32_t checksum = calculateChecksum(result);
if (checksum != entry.checksum) { if (checksum != entry.checksum) {
std::cerr << "Resource::Pack: checksum mismatch for: " << filename << '\n'; Log::error("Resource::Pack", "checksum mismatch for: {}", filename);
} }
return result; return result;
} }
+82 -38
View File
@@ -4,10 +4,8 @@
#include <algorithm> #include <algorithm>
#include <any> #include <any>
#include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
#include <iostream>
#include <memory> #include <memory>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -42,10 +40,11 @@
#include "game/scenes/title_scene.hpp" #include "game/scenes/title_scene.hpp"
#include "game/ui/service_menu.hpp" #include "game/ui/service_menu.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/log.hpp"
Director::Director() { Director::Director() {
if (!SDL_Init(0)) { if (!SDL_Init(0)) {
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); Log::critical("Director", "SDL_Init failed: {}", SDL_GetError());
} }
// Sistema de recursos: el pack viu al costat del binari (build/), el // Sistema de recursos: el pack viu al costat del binari (build/), el
@@ -66,11 +65,11 @@ Director::Director() {
constexpr bool ENABLE_FALLBACK = false; constexpr bool ENABLE_FALLBACK = false;
#endif #endif
if (!Resource::Helper::initializeResourceSystem(pack_file, ENABLE_FALLBACK)) { if (!Resource::Helper::initializeResourceSystem(pack_file, ENABLE_FALLBACK)) {
std::fprintf(stderr, "Director: sistema de recursos no disponible; avortem\n"); Log::critical("Director", "sistema de recursos no disponible; avortem");
std::exit(1); std::exit(1);
} }
// Prepara la carpeta de sistema (~/.config/jailgames/aee_2026/) i carrega // Prepara la carpeta de sistema (~/.config/jailgames/aee_arcade/) i carrega
// les opcions; si no existeix config.yaml el crea amb els defaults. // les opcions; si no existeix config.yaml el crea amb els defaults.
createSystemFolder(); createSystemFolder();
@@ -85,7 +84,7 @@ Director::Director() {
Options::setConfigFile(system_folder_ + "config.yaml"); Options::setConfigFile(system_folder_ + "config.yaml");
if (!Options::loadFromFile()) { if (!Options::loadFromFile()) {
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n'; Log::error("Director", "no s'ha pogut carregar ni desar config.yaml a {}", system_folder_);
} }
// Vinculacions de mando per dispositiu — fitxer aïllat de `config.yaml` // Vinculacions de mando per dispositiu — fitxer aïllat de `config.yaml`
@@ -97,12 +96,12 @@ Director::Director() {
// es crea amb presets per defecte (off, crt, ntsc). // es crea amb presets per defecte (off, crt, ntsc).
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE); Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
if (!Options::loadPostFXFromFile()) { if (!Options::loadPostFXFromFile()) {
std::cerr << "Director: no s'ha pogut carregar ni desar postfx.yaml.\n"; Log::error("Director", "no s'ha pogut carregar ni desar postfx.yaml.");
} }
// Presets del shader CRT-Pi (shaders/crtpi.yaml). // Presets del shader CRT-Pi (shaders/crtpi.yaml).
Options::setCrtPiFile(system_folder_ + Defaults::Video::CRTPI_FILE); Options::setCrtPiFile(system_folder_ + Defaults::Video::CRTPI_FILE);
if (!Options::loadCrtPiFromFile()) { if (!Options::loadCrtPiFromFile()) {
std::cerr << "Director: no s'ha pogut carregar ni desar crtpi.yaml.\n"; Log::error("Director", "no s'ha pogut carregar ni desar crtpi.yaml.");
} }
// Screen es configura ara (subsystem + finestra + GPU/renderer); la seua // Screen es configura ara (subsystem + finestra + GPU/renderer); la seua
@@ -118,22 +117,29 @@ Director::Director() {
Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale() + ".yaml"); Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale() + ".yaml");
// Hotkeys globals. GlobalInputs::update() les consumirà a cada frame. // Hotkeys globals. GlobalInputs::update() les consumirà a cada frame.
KeyConfig::get().load("data/input/keys.yaml"); KeyConfig::get().load(Defaults::Paths::INPUT_YAML);
// Tema d'UI (colors, timings, fonts, flags). Qualsevol clau absent a // Tema d'UI (colors, timings, fonts, flags). Qualsevol clau absent a
// data/ui.yaml cau al valor compilat a Defaults::UI. // data/config/ui.yaml cau al valor compilat a Defaults::UI.
UiConfig::get().load("data/ui.yaml"); UiConfig::get().load(Defaults::Paths::UI_YAML);
// Configuració específica de les escenes TITLE i LOGO. Es carreguen
// additivament sobre el mateix UiConfig: cada fitxer només conté la
// seua subclau (`title:` o `logo:`), i les claus absents a la base
// (ui.yaml) o als yamls d'escena cauen al valor compilat a
// Defaults::UI::Title / Defaults::UI::Logo.
UiConfig::get().load(Defaults::Paths::TITLE_YAML);
UiConfig::get().load(Defaults::Paths::LOGO_YAML);
#ifdef _DEBUG #ifdef _DEBUG
// Tema visual de l'editor de tilemap (Debug-only). Claus absents // Tema visual de l'editor de tilemap (Debug-only). Claus absents
// cauen als valors compilats a Defaults::Editor. // cauen als valors compilats a Defaults::Editor.
EditorConfig::get().load("data/editor/editor.yaml"); EditorConfig::get().load(Defaults::Paths::EDITOR_YAML);
#endif #endif
// Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els // Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els
// overlays perquè tots els reaprofiten sense duplicar la lògica. Els // overlays perquè tots els reaprofiten sense duplicar la lògica. Els
// noms de font viuen al fitxer data/ui.yaml via UiConfig. // noms de font viuen al fitxer data/config/ui.yaml via UiConfig.
Resource::Cache::get().loadPalette("ui", "data/palettes/uzebox.pal"); Resource::Cache::get().loadPalette("ui", Defaults::Paths::UI_PALETTE);
const std::string HUD_FONT_GIF_KEY = UiConfig::get().hudFont().name + ".gif"; const std::string HUD_FONT_GIF_KEY = UiConfig::get().hudFont().name + ".gif";
Resource::Cache::get().loadSurface(HUD_FONT_GIF_KEY, UiConfig::get().hudFont().gif); Resource::Cache::get().loadSurface(HUD_FONT_GIF_KEY, UiConfig::get().hudFont().gif);
Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui")); Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
@@ -152,29 +158,33 @@ Director::Director() {
// Pistes de música del cicle de sala (intro/playing/outro). Es preloden // Pistes de música del cicle de sala (intro/playing/outro). Es preloden
// ací perquè formen part del preload d'assets, com la font i la paleta; // ací perquè formen part del preload d'assets, com la font i la paleta;
// la lògica de quina sona en cada estat viu a `GameScene`. // la lògica de quina sona en cada estat viu a `GameScene`.
Resource::Cache::get().loadMusic("room_intro.ogg", "data/music/room_intro.ogg"); Resource::Cache::get().loadMusic("room_intro.ogg", Defaults::Paths::MUSIC_ROOM_INTRO);
Resource::Cache::get().loadMusic("playing.ogg", "data/music/playing.ogg"); Resource::Cache::get().loadMusic("playing.ogg", Defaults::Paths::MUSIC_PLAYING);
Resource::Cache::get().loadMusic("room_outro.ogg", "data/music/room_outro.ogg"); Resource::Cache::get().loadMusic("room_outro.ogg", Defaults::Paths::MUSIC_ROOM_OUTRO);
// Música de la TITLE: arrenca al constructor de l'escena i es fa // Música de la TITLE: arrenca al constructor de l'escena i es fa
// fade-out quan l'escena entra a STARTING. // fade-out quan l'escena entra a STARTING.
Resource::Cache::get().loadMusic("title.ogg", "data/music/title.ogg"); Resource::Cache::get().loadMusic("title.ogg", Defaults::Paths::MUSIC_TITLE);
// Sons del menú de servei. Els noms venen de `data/ui.yaml` (amb // Sons del menú de servei. Els noms venen de `data/config/ui.yaml` (amb
// fallback a `Defaults::UI::MENU_*_SOUND`); el ServiceMenu els // fallback a `Defaults::UI::MENU_*_SOUND`); el ServiceMenu els
// dispara via Audio::Group::INTERFACE. // dispara via Audio::Group::INTERFACE.
const auto& menu_select = UiConfig::get().menuSelectSound(); const auto& menu_select = UiConfig::get().menuSelectSound();
const auto& menu_accept = UiConfig::get().menuAcceptSound(); const auto& menu_accept = UiConfig::get().menuAcceptSound();
Resource::Cache::get().loadSound(menu_select, "data/sounds/" + menu_select); Resource::Cache::get().loadSound(menu_select, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + menu_select);
Resource::Cache::get().loadSound(menu_accept, "data/sounds/" + menu_accept); Resource::Cache::get().loadSound(menu_accept, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + menu_accept);
// So del START a la TITLE: el dispara TitleScene quan un jugador // So del START a la TITLE: el dispara TitleScene quan un jugador
// prem START (vore enterStarting i el reset del segon jugador). // prem START (vore enterStarting i el reset del segon jugador).
const auto& player_start = UiConfig::get().playerStartSound(); const auto& player_start = UiConfig::get().playerStartSound();
Resource::Cache::get().loadSound(player_start, "data/sounds/" + player_start); Resource::Cache::get().loadSound(player_start, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + player_start);
// Soroll de LogoScene (scanline reveal). El dispara LogoScene al
// ctor; amb fallback al nom de Defaults si la clau YAML no hi és.
const auto& logo_glitch = UiConfig::get().logoGlitchSound();
Resource::Cache::get().loadSound(logo_glitch, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + logo_glitch);
// Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio // Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio
// perquè els primers playSoundWith* (al començament del joc) ja // perquè els primers playSoundWith* (al començament del joc) ja
// troben els presets disponibles. // troben els presets disponibles.
SoundEffectsConfig::get().load("data/sounds.yaml"); SoundEffectsConfig::get().load(Defaults::Paths::SOUNDS_YAML);
// Audio: singleton manual propietari d'un SDL audio device. Es destrueix // Audio: singleton manual propietari d'un SDL audio device. Es destrueix
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els // explícitament a ~Director abans que Resource::Cache (Meyers) buide els
@@ -200,9 +210,19 @@ Director::Director() {
placeholder_text->setDefaultKerning(UiConfig::get().placeholderFont().kerning); placeholder_text->setDefaultKerning(UiConfig::get().placeholderFont().kerning);
placeholder_text_ = placeholder_text; placeholder_text_ = placeholder_text;
// Font del copyright de la TITLE (Tiny5-Regular size 8). Pixel-art
// estricte; va amb la paleta UI per compartir índex de color amb la
// resta de textos placeholder.
const std::string COPYRIGHT_FONT_GIF_KEY = UiConfig::get().copyrightFont().name + ".gif";
Resource::Cache::get().loadSurface(COPYRIGHT_FONT_GIF_KEY, UiConfig::get().copyrightFont().gif);
Resource::Cache::get().getSurface(COPYRIGHT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
auto copyright_text = std::make_shared<Text>(Resource::Cache::get().getSurface(COPYRIGHT_FONT_GIF_KEY), UiConfig::get().copyrightFont().fnt);
copyright_text->setDefaultKerning(UiConfig::get().copyrightFont().kerning);
copyright_text_ = copyright_text;
// Font del menú de servei (F9). El títol va amb la variant `display` // Font del menú de servei (F9). El títol va amb la variant `display`
// i els items amb `condensed-display` (més compacte perquè caben més // i els items amb `condensed-display` (més compacte perquè caben més
// files). Els dos noms i kernings viuen a data/ui.yaml via UiConfig. // files). Els dos noms i kernings viuen a data/config/ui.yaml via UiConfig.
auto load_menu_font = [&](const UiConfig::Font& font) { auto load_menu_font = [&](const UiConfig::Font& font) {
const std::string KEY = font.name + ".gif"; const std::string KEY = font.name + ".gif";
Resource::Cache::get().loadSurface(KEY, font.gif); Resource::Cache::get().loadSurface(KEY, font.gif);
@@ -213,21 +233,34 @@ Director::Director() {
}; };
auto menu_title_text = load_menu_font(UiConfig::get().menuTitleFont()); auto menu_title_text = load_menu_font(UiConfig::get().menuTitleFont());
auto menu_items_text = load_menu_font(UiConfig::get().menuItemsFont()); auto menu_items_text = load_menu_font(UiConfig::get().menuItemsFont());
menu_title_text_ = menu_title_text;
// Mini-logo del peu de la TITLE. Es preloda ací per consistència // Mini-logo del peu de la TITLE. Es preloda ací per consistència
// amb la resta d'assets d'UI; manté la seua paleta original (no la // amb la resta d'assets d'UI; manté la seua paleta original (no la
// sobreescrivim amb la "ui" perquè el GIF ja porta els colors finals // sobreescrivim amb la "ui" perquè el GIF ja porta els colors finals
// del logo). // del logo).
constexpr const char* MINI_LOGO_KEY = "logo_jailgames_mini.gif"; constexpr const char* MINI_LOGO_KEY = "logo_jailgames_mini.gif";
Resource::Cache::get().loadSurface(MINI_LOGO_KEY, "data/logo/logo_jailgames_mini.gif"); Resource::Cache::get().loadSurface(MINI_LOGO_KEY, Defaults::Paths::LOGO_MINI_GIF);
auto mini_logo_surface = Resource::Cache::get().getSurface(MINI_LOGO_KEY); auto mini_logo_surface = Resource::Cache::get().getSurface(MINI_LOGO_KEY);
mini_logo_surface_ = mini_logo_surface; mini_logo_surface_ = mini_logo_surface;
// Logo sencer per a l'escena LOGO (centrat damunt fons negre). També
// conserva la seua paleta nadiua: els colors finals del logo viuen
// dins del propi GIF i no han de passar per la paleta UI.
constexpr const char* LOGO_FULL_KEY = "logo_jailgames.gif";
Resource::Cache::get().loadSurface(LOGO_FULL_KEY, Defaults::Paths::LOGO_FULL_GIF);
logo_full_surface_ = Resource::Cache::get().getSurface(LOGO_FULL_KEY);
// Logotip del joc per a la TITLE (substitueix el text "Aventures en
// Egipte ARCADE"). Igual que la resta de logos, paleta nadiua —
// l'art ja porta els colors finals i no passa per la UI.
constexpr const char* GAME_LOGO_KEY = "game_logo.gif";
Resource::Cache::get().loadSurface(GAME_LOGO_KEY, Defaults::Paths::TITLE_GAME_LOGO_GIF);
game_logo_surface_ = Resource::Cache::get().getSurface(GAME_LOGO_KEY);
// Fons sencer de la TITLE (320×240). Igual que el minilogo, conserva // Fons sencer de la TITLE (320×240). Igual que el minilogo, conserva
// la seua paleta nadiua perquè el GIF ja porta els colors finals. // la seua paleta nadiua perquè el GIF ja porta els colors finals.
constexpr const char* TITLE_BG_KEY = "title_bg.gif"; constexpr const char* TITLE_BG_KEY = "title_bg.gif";
Resource::Cache::get().loadSurface(TITLE_BG_KEY, "data/title/title_bg.gif"); Resource::Cache::get().loadSurface(TITLE_BG_KEY, Defaults::Paths::TITLE_BG_GIF);
auto title_bg_surface = Resource::Cache::get().getSurface(TITLE_BG_KEY); auto title_bg_surface = Resource::Cache::get().getSurface(TITLE_BG_KEY);
title_bg_surface_ = title_bg_surface; title_bg_surface_ = title_bg_surface;
ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette); ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette);
@@ -261,8 +294,7 @@ Director::Director() {
const auto START = DebugConfig::StartScene::LOGO; const auto START = DebugConfig::StartScene::LOGO;
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH; const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
#endif #endif
std::cout << "Director: start_scene=" << DebugConfig::toString(START) Log::info("Director", "start_scene={} starting_players={}", DebugConfig::toString(START), DebugConfig::toString(STARTING_PLAYERS));
<< " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n';
switch (START) { switch (START) {
case DebugConfig::StartScene::LOGO: case DebugConfig::StartScene::LOGO:
@@ -299,8 +331,8 @@ Director::~Director() {
} }
void Director::createSystemFolder() { void Director::createSystemFolder() {
// Volem la config a $XDG_CONFIG_HOME/jailgames/aee_2026/ (defecte // Volem la config a $XDG_CONFIG_HOME/jailgames/aee_arcade/ (defecte
// ~/.config/jailgames/aee_2026/). SDL_GetPrefPath apunta a XDG_DATA_HOME // ~/.config/jailgames/aee_arcade/). SDL_GetPrefPath apunta a XDG_DATA_HOME
// (.local/share), que és el lloc per a dades, no per a configuració. // (.local/share), que és el lloc per a dades, no per a configuració.
const char* xdg = std::getenv("XDG_CONFIG_HOME"); const char* xdg = std::getenv("XDG_CONFIG_HOME");
std::string base; std::string base;
@@ -310,14 +342,14 @@ void Director::createSystemFolder() {
const char* home = std::getenv("HOME"); const char* home = std::getenv("HOME");
base = (home != nullptr) ? std::string(home) + "/.config" : "."; base = (home != nullptr) ? std::string(home) + "/.config" : ".";
} }
system_folder_ = base + "/jailgames/aee_2026/"; system_folder_ = base + "/jailgames/aee_arcade/";
std::error_code error; std::error_code error;
std::filesystem::create_directories(system_folder_, error); std::filesystem::create_directories(system_folder_, error);
// Subcarpeta per als presets de shaders. std::filesystem és tolerant si ja existeix. // Subcarpeta per als presets de shaders. std::filesystem és tolerant si ja existeix.
std::filesystem::create_directories(system_folder_ + "shaders/", error); std::filesystem::create_directories(system_folder_ + "shaders/", error);
if (error) { if (error) {
std::cerr << "Director: no s'ha pogut crear " << system_folder_ << ": " << error.message() << '\n'; Log::error("Director", "no s'ha pogut crear {}: {}", system_folder_, error.message());
} }
} }
@@ -393,6 +425,16 @@ void Director::processMenuInput() {
void Director::switchSceneIfRequested() { void Director::switchSceneIfRequested() {
if (!current_scene_) { return; } if (!current_scene_) { return; }
// Prioritat al menú: si un item del ServiceMenu ha encuat una
// transició (típicament "Reiniciar" → LOGO amb reset), s'aplica
// primer. Substitueix l'`ExitRequest` que pugui haver publicat
// l'escena en aquest mateix frame — l'usuari ha cancel·lat l'estat
// de joc explícitament i no ens interessa mantenir-ne la sortida.
if (auto menu_req = ServiceMenu::get().takePendingSceneRequest(); menu_req.has_value()) {
Log::info("Director", "scene transition (menu) → {}", toString(menu_req->next));
current_scene_ = buildScene(menu_req->next, menu_req->params);
return;
}
// L'escena ha publicat un `ExitRequest` dins de `exit_request_`. // L'escena ha publicat un `ExitRequest` dins de `exit_request_`.
// Director el desempaqueta i construeix la nova escena via // Director el desempaqueta i construeix la nova escena via
// `buildScene`. Si la `std::any_cast` falla (tipus inesperat), el // `buildScene`. Si la `std::any_cast` falla (tipus inesperat), el
@@ -401,10 +443,10 @@ void Director::switchSceneIfRequested() {
if (!request.has_value()) { return; } if (!request.has_value()) { return; }
try { try {
const auto& req = std::any_cast<const ExitRequest&>(request); const auto& req = std::any_cast<const ExitRequest&>(request);
std::cout << "Director: scene transition → " << toString(req.next) << '\n'; Log::info("Director", "scene transition → {}", toString(req.next));
current_scene_ = buildScene(req.next, req.params); current_scene_ = buildScene(req.next, req.params);
} catch (const std::bad_any_cast&) { } catch (const std::bad_any_cast&) {
std::cerr << "Director: exit_request_ amb tipus inesperat — ignorant\n"; Log::error("Director", "exit_request_ amb tipus inesperat — ignorant");
} }
} }
@@ -415,10 +457,12 @@ auto Director::buildScene(SceneId id, const SceneParams& params) -> std::unique_
// ctors antics no l'utilitzen directament. Quan totes estiguen // ctors antics no l'utilitzen directament. Quan totes estiguen
// migrades a `ExitRequest`, aquestes lambdes desapareixeran. // migrades a `ExitRequest`, aquestes lambdes desapareixeran.
switch (id) { switch (id) {
case SceneId::LOGO: case SceneId::LOGO: {
return std::make_unique<LogoScene>(placeholder_text_); const auto& p = std::get<LogoParams>(params);
return std::make_unique<LogoScene>(logo_full_surface_, p.reset);
}
case SceneId::TITLE: case SceneId::TITLE:
return std::make_unique<TitleScene>(menu_title_text_, placeholder_text_, title_bg_surface_, mini_logo_surface_); return std::make_unique<TitleScene>(placeholder_text_, copyright_text_, title_bg_surface_, game_logo_surface_, mini_logo_surface_);
case SceneId::INTRO: { case SceneId::INTRO: {
const auto& p = std::get<IntroParams>(params); const auto& p = std::get<IntroParams>(params);
return std::make_unique<IntroScene>(placeholder_text_, p.starting_players); return std::make_unique<IntroScene>(placeholder_text_, p.starting_players);
+3 -1
View File
@@ -30,9 +30,11 @@ class Director {
// escena. No són `static`/Meyers perquè depenen de l'inici d'SDL i // escena. No són `static`/Meyers perquè depenen de l'inici d'SDL i
// de Resource::Cache. // de Resource::Cache.
std::shared_ptr<Text> placeholder_text_; std::shared_ptr<Text> placeholder_text_;
std::shared_ptr<Text> menu_title_text_; std::shared_ptr<Text> copyright_text_;
std::shared_ptr<Surface> title_bg_surface_; std::shared_ptr<Surface> title_bg_surface_;
std::shared_ptr<Surface> game_logo_surface_;
std::shared_ptr<Surface> mini_logo_surface_; std::shared_ptr<Surface> mini_logo_surface_;
std::shared_ptr<Surface> logo_full_surface_;
void createSystemFolder(); void createSystemFolder();
void processEvents(); void processEvents();
+91 -8
View File
@@ -1,6 +1,5 @@
#include "core/ui/ui_config.hpp" #include "core/ui/ui_config.hpp"
#include <iostream>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <unordered_map> #include <unordered_map>
@@ -9,6 +8,7 @@
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/easing.hpp" #include "utils/easing.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -27,7 +27,7 @@ namespace {
if (!node.contains(key)) { return; } if (!node.contains(key)) { return; }
const int VALUE = node[key].get_value<int>(); const int VALUE = node[key].get_value<int>();
if (VALUE < 0 || VALUE > 255) { if (VALUE < 0 || VALUE > 255) {
std::cerr << "UiConfig: índex '" << key << "'=" << VALUE << " fora de rang 0..255 — ignorat\n"; Log::warn("UiConfig", "índex '{}'={} fora de rang 0..255 — ignorat", key, VALUE);
return; return;
} }
dst = static_cast<Uint8>(VALUE); dst = static_cast<Uint8>(VALUE);
@@ -100,6 +100,11 @@ UiConfig::UiConfig()
title_bg_color_(Defaults::UI::Title::BG_COLOR), title_bg_color_(Defaults::UI::Title::BG_COLOR),
title_text_color_(Defaults::UI::Title::TEXT_COLOR), title_text_color_(Defaults::UI::Title::TEXT_COLOR),
title_copyright_color_(Defaults::UI::Title::COPYRIGHT_COLOR), title_copyright_color_(Defaults::UI::Title::COPYRIGHT_COLOR),
title_press_start_outline_color_(Defaults::UI::Title::PRESS_START_OUTLINE_COLOR),
title_logo_y_percent_(Defaults::UI::Title::LOGO_Y_PERCENT),
title_prompt_y_percent_(Defaults::UI::Title::PROMPT_Y_PERCENT),
title_copyright_y_percent_(Defaults::UI::Title::COPYRIGHT_Y_PERCENT),
title_mini_logo_gap_px_(Defaults::UI::Title::MINI_LOGO_GAP_PX),
title_blink_visible_ms_(Defaults::UI::Title::BLINK_VISIBLE_MS), title_blink_visible_ms_(Defaults::UI::Title::BLINK_VISIBLE_MS),
title_blink_hidden_ms_(Defaults::UI::Title::BLINK_HIDDEN_MS), title_blink_hidden_ms_(Defaults::UI::Title::BLINK_HIDDEN_MS),
title_starting_ms_(Defaults::UI::Title::STARTING_MS), title_starting_ms_(Defaults::UI::Title::STARTING_MS),
@@ -108,21 +113,54 @@ UiConfig::UiConfig()
title_fade_out_max_block_(Defaults::UI::Title::FADE_OUT_MAX_BLOCK), title_fade_out_max_block_(Defaults::UI::Title::FADE_OUT_MAX_BLOCK),
title_music_fade_out_ms_(Defaults::UI::Title::MUSIC_FADE_OUT_MS), title_music_fade_out_ms_(Defaults::UI::Title::MUSIC_FADE_OUT_MS),
title_attract_fade_to_black_ms_(Defaults::UI::Title::ATTRACT_FADE_TO_BLACK_MS), title_attract_fade_to_black_ms_(Defaults::UI::Title::ATTRACT_FADE_TO_BLACK_MS),
title_intro_fade_in_ms_(Defaults::UI::Title::INTRO_FADE_IN_MS), title_intro_{
.fade_in_ms = Defaults::UI::Title::Intro::FADE_IN_MS,
.game_logo_at_ms = Defaults::UI::Title::Intro::GAME_LOGO_AT_MS,
.mini_logo_at_ms = Defaults::UI::Title::Intro::MINI_LOGO_AT_MS,
.mini_logo_slide_ms = Defaults::UI::Title::Intro::MINI_LOGO_SLIDE_MS,
.copyright_at_ms = Defaults::UI::Title::Intro::COPYRIGHT_AT_MS,
.copyright_slide_ms = Defaults::UI::Title::Intro::COPYRIGHT_SLIDE_MS,
.slide_curve = curveByName(Defaults::UI::Title::Intro::SLIDE_CURVE),
.press_start_at_ms = Defaults::UI::Title::Intro::PRESS_START_AT_MS,
},
title_heat_haze_{ title_heat_haze_{
.enabled = Defaults::UI::Title::HeatHaze::ENABLED, .enabled = Defaults::UI::Title::HeatHaze::ENABLED,
.amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX, .amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX,
.wavelength_px = Defaults::UI::Title::HeatHaze::WAVELENGTH_PX, .wavelength_px = Defaults::UI::Title::HeatHaze::WAVELENGTH_PX,
.speed_hz = Defaults::UI::Title::HeatHaze::SPEED_HZ, .speed_hz = Defaults::UI::Title::HeatHaze::SPEED_HZ,
}, },
logo_scan_{
.duration_ms = Defaults::UI::Logo::Scan::DURATION_MS,
.tail_glitch_ms = Defaults::UI::Logo::Scan::TAIL_GLITCH_MS,
.band_height_px = Defaults::UI::Logo::Scan::BAND_HEIGHT_PX,
.band_alpha = Defaults::UI::Logo::Scan::BAND_ALPHA,
.band_pulse_hz = Defaults::UI::Logo::Scan::BAND_PULSE_HZ,
.band_pulse_depth = Defaults::UI::Logo::Scan::BAND_PULSE_DEPTH,
.glitch_amplitude_px = Defaults::UI::Logo::Scan::GLITCH_AMPLITUDE_PX,
.glitch_band_radius_px = Defaults::UI::Logo::Scan::GLITCH_BAND_RADIUS_PX,
.vhs_burst_per_sec = Defaults::UI::Logo::Scan::VHS_BURST_PER_SEC,
.vhs_burst_duration_ms = Defaults::UI::Logo::Scan::VHS_BURST_DURATION_MS,
.vhs_burst_height_px = Defaults::UI::Logo::Scan::VHS_BURST_HEIGHT_PX,
.vhs_burst_amplitude_px = Defaults::UI::Logo::Scan::VHS_BURST_AMPLITUDE_PX,
.flash_ms = Defaults::UI::Logo::Scan::FLASH_MS,
},
logo_timeline_{
.black_intro_ms = Defaults::UI::Logo::Timeline::BLACK_INTRO_MS,
.hold_silent_ms = Defaults::UI::Logo::Timeline::HOLD_SILENT_MS,
.hold_with_music_ms = Defaults::UI::Logo::Timeline::HOLD_WITH_MUSIC_MS,
.fade_out_ms = Defaults::UI::Logo::Timeline::FADE_OUT_MS,
.black_outro_ms = Defaults::UI::Logo::Timeline::BLACK_OUTRO_MS,
},
menu_select_sound_(Defaults::UI::MENU_SELECT_SOUND), menu_select_sound_(Defaults::UI::MENU_SELECT_SOUND),
menu_accept_sound_(Defaults::UI::MENU_ACCEPT_SOUND), menu_accept_sound_(Defaults::UI::MENU_ACCEPT_SOUND),
player_start_sound_(Defaults::UI::PLAYER_START_SOUND), player_start_sound_(Defaults::UI::PLAYER_START_SOUND),
logo_glitch_sound_(Defaults::UI::LOGO_GLITCH_SOUND),
hud_font_{.name = Defaults::UI::Font::HUD_NAME, .gif = Defaults::UI::Font::HUD_GIF, .fnt = Defaults::UI::Font::HUD_FNT, .kerning = Defaults::UI::Font::HUD_KERNING}, hud_font_{.name = Defaults::UI::Font::HUD_NAME, .gif = Defaults::UI::Font::HUD_GIF, .fnt = Defaults::UI::Font::HUD_FNT, .kerning = Defaults::UI::Font::HUD_KERNING},
placeholder_font_{.name = Defaults::UI::Font::PLACEHOLDER_NAME, .gif = Defaults::UI::Font::PLACEHOLDER_GIF, .fnt = Defaults::UI::Font::PLACEHOLDER_FNT, .kerning = Defaults::UI::Font::PLACEHOLDER_KERNING}, placeholder_font_{.name = Defaults::UI::Font::PLACEHOLDER_NAME, .gif = Defaults::UI::Font::PLACEHOLDER_GIF, .fnt = Defaults::UI::Font::PLACEHOLDER_FNT, .kerning = Defaults::UI::Font::PLACEHOLDER_KERNING},
room_text_font_{.name = Defaults::UI::Font::ROOM_TEXT_NAME, .gif = Defaults::UI::Font::ROOM_TEXT_GIF, .fnt = Defaults::UI::Font::ROOM_TEXT_FNT, .kerning = Defaults::UI::Font::ROOM_TEXT_KERNING}, room_text_font_{.name = Defaults::UI::Font::ROOM_TEXT_NAME, .gif = Defaults::UI::Font::ROOM_TEXT_GIF, .fnt = Defaults::UI::Font::ROOM_TEXT_FNT, .kerning = Defaults::UI::Font::ROOM_TEXT_KERNING},
menu_title_font_{.name = Defaults::UI::Font::MENU_TITLE_NAME, .gif = Defaults::UI::Font::MENU_TITLE_GIF, .fnt = Defaults::UI::Font::MENU_TITLE_FNT, .kerning = Defaults::UI::Font::MENU_TITLE_KERNING}, menu_title_font_{.name = Defaults::UI::Font::MENU_TITLE_NAME, .gif = Defaults::UI::Font::MENU_TITLE_GIF, .fnt = Defaults::UI::Font::MENU_TITLE_FNT, .kerning = Defaults::UI::Font::MENU_TITLE_KERNING},
menu_items_font_{.name = Defaults::UI::Font::MENU_ITEMS_NAME, .gif = Defaults::UI::Font::MENU_ITEMS_GIF, .fnt = Defaults::UI::Font::MENU_ITEMS_FNT, .kerning = Defaults::UI::Font::MENU_ITEMS_KERNING}, menu_items_font_{.name = Defaults::UI::Font::MENU_ITEMS_NAME, .gif = Defaults::UI::Font::MENU_ITEMS_GIF, .fnt = Defaults::UI::Font::MENU_ITEMS_FNT, .kerning = Defaults::UI::Font::MENU_ITEMS_KERNING},
copyright_font_{.name = Defaults::UI::Font::COPYRIGHT_NAME, .gif = Defaults::UI::Font::COPYRIGHT_GIF, .fnt = Defaults::UI::Font::COPYRIGHT_FNT, .kerning = Defaults::UI::Font::COPYRIGHT_KERNING},
notifier_{ notifier_{
.padding_h = Defaults::UI::Notifier::PADDING_H, .padding_h = Defaults::UI::Notifier::PADDING_H,
.padding_top = Defaults::UI::Notifier::PADDING_TOP, .padding_top = Defaults::UI::Notifier::PADDING_TOP,
@@ -145,7 +183,7 @@ auto UiConfig::curveByName(const std::string& name) -> CurveFn {
const auto& reg = curveRegistry(); const auto& reg = curveRegistry();
const auto IT = reg.find(name); const auto IT = reg.find(name);
if (IT == reg.end()) { if (IT == reg.end()) {
std::cerr << "UiConfig: corba '" << name << "' desconeguda — s'usa linear\n"; Log::warn("UiConfig", "corba '{}' desconeguda — s'usa linear", name);
return &Easing::linear; return &Easing::linear;
} }
return IT->second; return IT->second;
@@ -154,7 +192,7 @@ auto UiConfig::curveByName(const std::string& name) -> CurveFn {
void UiConfig::load(const std::string& file_path) { void UiConfig::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path); auto bytes = Resource::Helper::loadFile(file_path);
if (bytes.empty()) { if (bytes.empty()) {
std::cerr << "UiConfig: no s'ha pogut obrir " << file_path << " — s'usaran els defaults compilats\n"; Log::warn("UiConfig", "no s'ha pogut obrir {} — s'usaran els defaults compilats", file_path);
return; return;
} }
@@ -200,6 +238,7 @@ void UiConfig::load(const std::string& file_path) {
readFont(f, "room_text", room_text_font_); readFont(f, "room_text", room_text_font_);
readFont(f, "menu_title", menu_title_font_); readFont(f, "menu_title", menu_title_font_);
readFont(f, "menu_items", menu_items_font_); readFont(f, "menu_items", menu_items_font_);
readFont(f, "copyright", copyright_font_);
} }
if (yaml.contains("hud")) { if (yaml.contains("hud")) {
@@ -223,6 +262,7 @@ void UiConfig::load(const std::string& file_path) {
readField(s, "menu_select", menu_select_sound_); readField(s, "menu_select", menu_select_sound_);
readField(s, "menu_accept", menu_accept_sound_); readField(s, "menu_accept", menu_accept_sound_);
readField(s, "player_start", player_start_sound_); readField(s, "player_start", player_start_sound_);
readField(s, "logo_glitch", logo_glitch_sound_);
} }
if (yaml.contains("title")) { if (yaml.contains("title")) {
@@ -231,6 +271,11 @@ void UiConfig::load(const std::string& file_path) {
readColorIndex(t, "bg_color", title_bg_color_); readColorIndex(t, "bg_color", title_bg_color_);
readColorIndex(t, "text_color", title_text_color_); readColorIndex(t, "text_color", title_text_color_);
readColorIndex(t, "copyright_color", title_copyright_color_); readColorIndex(t, "copyright_color", title_copyright_color_);
readColorIndex(t, "press_start_outline_color", title_press_start_outline_color_);
readField(t, "logo_y_percent", title_logo_y_percent_);
readField(t, "prompt_y_percent", title_prompt_y_percent_);
readField(t, "copyright_y_percent", title_copyright_y_percent_);
readField(t, "mini_logo_gap_px", title_mini_logo_gap_px_);
readField(t, "blink_visible_ms", title_blink_visible_ms_); readField(t, "blink_visible_ms", title_blink_visible_ms_);
readField(t, "blink_hidden_ms", title_blink_hidden_ms_); readField(t, "blink_hidden_ms", title_blink_hidden_ms_);
readField(t, "starting_ms", title_starting_ms_); readField(t, "starting_ms", title_starting_ms_);
@@ -239,7 +284,17 @@ void UiConfig::load(const std::string& file_path) {
readField(t, "fade_out_max_block", title_fade_out_max_block_); readField(t, "fade_out_max_block", title_fade_out_max_block_);
readField(t, "music_fade_out_ms", title_music_fade_out_ms_); readField(t, "music_fade_out_ms", title_music_fade_out_ms_);
readField(t, "attract_fade_to_black_ms", title_attract_fade_to_black_ms_); readField(t, "attract_fade_to_black_ms", title_attract_fade_to_black_ms_);
readField(t, "intro_fade_in_ms", title_intro_fade_in_ms_); if (t.contains("intro")) {
const auto& in = t["intro"];
readField(in, "fade_in_ms", title_intro_.fade_in_ms);
readField(in, "game_logo_at_ms", title_intro_.game_logo_at_ms);
readField(in, "mini_logo_at_ms", title_intro_.mini_logo_at_ms);
readField(in, "mini_logo_slide_ms", title_intro_.mini_logo_slide_ms);
readField(in, "copyright_at_ms", title_intro_.copyright_at_ms);
readField(in, "copyright_slide_ms", title_intro_.copyright_slide_ms);
readCurve(in, "slide_curve", title_intro_.slide_curve);
readField(in, "press_start_at_ms", title_intro_.press_start_at_ms);
}
if (t.contains("heat_haze")) { if (t.contains("heat_haze")) {
const auto& hh = t["heat_haze"]; const auto& hh = t["heat_haze"];
readField(hh, "enabled", title_heat_haze_.enabled); readField(hh, "enabled", title_heat_haze_.enabled);
@@ -249,6 +304,34 @@ void UiConfig::load(const std::string& file_path) {
} }
} }
if (yaml.contains("logo")) {
const auto& l = yaml["logo"];
if (l.contains("scan")) {
const auto& s = l["scan"];
readField(s, "duration_ms", logo_scan_.duration_ms);
readField(s, "tail_glitch_ms", logo_scan_.tail_glitch_ms);
readField(s, "band_height_px", logo_scan_.band_height_px);
readField(s, "band_alpha", logo_scan_.band_alpha);
readField(s, "band_pulse_hz", logo_scan_.band_pulse_hz);
readField(s, "band_pulse_depth", logo_scan_.band_pulse_depth);
readField(s, "glitch_amplitude_px", logo_scan_.glitch_amplitude_px);
readField(s, "glitch_band_radius_px", logo_scan_.glitch_band_radius_px);
readField(s, "vhs_burst_per_sec", logo_scan_.vhs_burst_per_sec);
readField(s, "vhs_burst_duration_ms", logo_scan_.vhs_burst_duration_ms);
readField(s, "vhs_burst_height_px", logo_scan_.vhs_burst_height_px);
readField(s, "vhs_burst_amplitude_px", logo_scan_.vhs_burst_amplitude_px);
readField(s, "flash_ms", logo_scan_.flash_ms);
}
if (l.contains("timeline")) {
const auto& tl = l["timeline"];
readField(tl, "black_intro_ms", logo_timeline_.black_intro_ms);
readField(tl, "hold_silent_ms", logo_timeline_.hold_silent_ms);
readField(tl, "hold_with_music_ms", logo_timeline_.hold_with_music_ms);
readField(tl, "fade_out_ms", logo_timeline_.fade_out_ms);
readField(tl, "black_outro_ms", logo_timeline_.black_outro_ms);
}
}
if (yaml.contains("notifier")) { if (yaml.contains("notifier")) {
const auto& n = yaml["notifier"]; const auto& n = yaml["notifier"];
readField(n, "padding_h", notifier_.padding_h); readField(n, "padding_h", notifier_.padding_h);
@@ -262,8 +345,8 @@ void UiConfig::load(const std::string& file_path) {
readCurve(n, "slide_out_curve", notifier_.slide_out_curve); readCurve(n, "slide_out_curve", notifier_.slide_out_curve);
} }
std::cout << "UiConfig: carregat des de " << file_path << '\n'; Log::info("UiConfig", "carregat des de {}", file_path);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "UiConfig: error parsejant " << file_path << ": " << e.what() << '\n'; Log::error("UiConfig", "error parsejant {}: {}", file_path, e.what());
} }
} }
+71 -6
View File
@@ -5,7 +5,7 @@
#include <string> #include <string>
// Configuració del tema UI (índexs de paleta, timings, fonts i flags) // Configuració del tema UI (índexs de paleta, timings, fonts i flags)
// carregada des de data/ui.yaml. Cada camp s'inicialitza amb el valor // carregada des de data/config/ui.yaml. Cada camp s'inicialitza amb el valor
// de Defaults::UI com a fallback: si el fitxer falta o li manca una // de Defaults::UI com a fallback: si el fitxer falta o li manca una
// clau, el joc s'executa amb el tema compilat. // clau, el joc s'executa amb el tema compilat.
// //
@@ -21,12 +21,12 @@ class UiConfig {
}; };
// Signatura d'una funció d'easing: t ∈ [0, 1] → valor ∈ [0, 1]. // Signatura d'una funció d'easing: t ∈ [0, 1] → valor ∈ [0, 1].
// Permet triar corbes des de data/ui.yaml per nom (cadena) i guardar // Permet triar corbes des de data/config/ui.yaml per nom (cadena) i guardar
// el punter a la corresponent funció de `utils/easing.hpp`. // el punter a la corresponent funció de `utils/easing.hpp`.
using CurveFn = auto (*)(float) -> float; using CurveFn = auto (*)(float) -> float;
// Geometria del Notifier (popup superior). Carregada des de // Geometria del Notifier (popup superior). Carregada des de
// data/ui.yaml secció `notifier:`; els valors per defecte vénen de // data/config/ui.yaml secció `notifier:`; els valors per defecte vénen de
// Defaults::UI::Notifier. El moviment és per temps (slide_ms) i no per // Defaults::UI::Notifier. El moviment és per temps (slide_ms) i no per
// velocitat, perquè una caixa més gran no triga més a entrar/eixir. // velocitat, perquè una caixa més gran no triga més a entrar/eixir.
struct Notifier { struct Notifier {
@@ -89,10 +89,29 @@ class UiConfig {
float speed_hz; float speed_hz;
}; };
// Seqüència d'aparició dels elements en fase IDLE: tots els temps
// són absoluts des de l'entrada a l'escena. `slide_curve` es
// comparteix entre el slide-in del mini-logo i el del copyright.
struct TitleIntro {
int fade_in_ms;
int game_logo_at_ms;
int mini_logo_at_ms;
int mini_logo_slide_ms;
int copyright_at_ms;
int copyright_slide_ms;
CurveFn slide_curve;
int press_start_at_ms;
};
[[nodiscard]] auto titleDurationMs() const -> int { return title_duration_ms_; } [[nodiscard]] auto titleDurationMs() const -> int { return title_duration_ms_; }
[[nodiscard]] auto titleBgColor() const -> Uint8 { return title_bg_color_; } [[nodiscard]] auto titleBgColor() const -> Uint8 { return title_bg_color_; }
[[nodiscard]] auto titleTextColor() const -> Uint8 { return title_text_color_; } [[nodiscard]] auto titleTextColor() const -> Uint8 { return title_text_color_; }
[[nodiscard]] auto titleCopyrightColor() const -> Uint8 { return title_copyright_color_; } [[nodiscard]] auto titleCopyrightColor() const -> Uint8 { return title_copyright_color_; }
[[nodiscard]] auto titlePressStartOutlineColor() const -> Uint8 { return title_press_start_outline_color_; }
[[nodiscard]] auto titleLogoYPercent() const -> int { return title_logo_y_percent_; }
[[nodiscard]] auto titlePromptYPercent() const -> int { return title_prompt_y_percent_; }
[[nodiscard]] auto titleCopyrightYPercent() const -> int { return title_copyright_y_percent_; }
[[nodiscard]] auto titleMiniLogoGapPx() const -> int { return title_mini_logo_gap_px_; }
[[nodiscard]] auto titleBlinkVisibleMs() const -> int { return title_blink_visible_ms_; } [[nodiscard]] auto titleBlinkVisibleMs() const -> int { return title_blink_visible_ms_; }
[[nodiscard]] auto titleBlinkHiddenMs() const -> int { return title_blink_hidden_ms_; } [[nodiscard]] auto titleBlinkHiddenMs() const -> int { return title_blink_hidden_ms_; }
[[nodiscard]] auto titleStartingMs() const -> int { return title_starting_ms_; } [[nodiscard]] auto titleStartingMs() const -> int { return title_starting_ms_; }
@@ -101,9 +120,44 @@ class UiConfig {
[[nodiscard]] auto titleFadeOutMaxBlock() const -> int { return title_fade_out_max_block_; } [[nodiscard]] auto titleFadeOutMaxBlock() const -> int { return title_fade_out_max_block_; }
[[nodiscard]] auto titleMusicFadeOutMs() const -> int { return title_music_fade_out_ms_; } [[nodiscard]] auto titleMusicFadeOutMs() const -> int { return title_music_fade_out_ms_; }
[[nodiscard]] auto titleAttractFadeToBlackMs() const -> int { return title_attract_fade_to_black_ms_; } [[nodiscard]] auto titleAttractFadeToBlackMs() const -> int { return title_attract_fade_to_black_ms_; }
[[nodiscard]] auto titleIntroFadeInMs() const -> int { return title_intro_fade_in_ms_; } [[nodiscard]] auto titleIntro() const -> const TitleIntro& { return title_intro_; }
[[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; } [[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; }
// Escena LOGO — paràmetres del scanline reveal del logo de l'editora.
// `duration_ms` és la durada de la línia descendent; `tail_glitch_ms`
// és el temps post-flash en què el logo queda totalment visible
// però els bursts VHS encara hi salten (glitch tail).
struct LogoScan {
int duration_ms;
int tail_glitch_ms;
int band_height_px;
int band_alpha;
float band_pulse_hz;
float band_pulse_depth;
int glitch_amplitude_px;
int glitch_band_radius_px;
int vhs_burst_per_sec;
int vhs_burst_duration_ms;
int vhs_burst_height_px;
int vhs_burst_amplitude_px;
int flash_ms;
};
[[nodiscard]] auto logoScan() const -> const LogoScan& { return logo_scan_; }
// Timeline lineal de la LogoScene. Cada camp és la durada (ms) d'una
// fase. La SCAN no apareix aquí perquè la marca el propi
// `LogoScan` (duration_ms + tail_glitch_ms + flash_ms).
struct LogoTimeline {
int black_intro_ms;
int hold_silent_ms;
int hold_with_music_ms;
int fade_out_ms;
int black_outro_ms;
};
[[nodiscard]] auto logoTimeline() const -> const LogoTimeline& { return logo_timeline_; }
// Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa // Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa
// Easing::linear com a fallback. // Easing::linear com a fallback.
[[nodiscard]] auto pauseEnterCurve() const -> CurveFn { return pause_enter_curve_; } [[nodiscard]] auto pauseEnterCurve() const -> CurveFn { return pause_enter_curve_; }
@@ -116,10 +170,11 @@ class UiConfig {
[[nodiscard]] static auto curveByName(const std::string& name) -> CurveFn; [[nodiscard]] static auto curveByName(const std::string& name) -> CurveFn;
// Sons del menú de servei. Claus de Resource::Cache::loadSound (els // Sons del menú de servei. Claus de Resource::Cache::loadSound (els
// fitxers viuen a `data/sounds/`). // fitxers viuen a `data/audio/sounds/`).
[[nodiscard]] auto menuSelectSound() const -> const std::string& { return menu_select_sound_; } [[nodiscard]] auto menuSelectSound() const -> const std::string& { return menu_select_sound_; }
[[nodiscard]] auto menuAcceptSound() const -> const std::string& { return menu_accept_sound_; } [[nodiscard]] auto menuAcceptSound() const -> const std::string& { return menu_accept_sound_; }
[[nodiscard]] auto playerStartSound() const -> const std::string& { return player_start_sound_; } [[nodiscard]] auto playerStartSound() const -> const std::string& { return player_start_sound_; }
[[nodiscard]] auto logoGlitchSound() const -> const std::string& { return logo_glitch_sound_; }
// Fonts de UI. // Fonts de UI.
[[nodiscard]] auto hudFont() const -> const Font& { return hud_font_; } [[nodiscard]] auto hudFont() const -> const Font& { return hud_font_; }
@@ -127,6 +182,7 @@ class UiConfig {
[[nodiscard]] auto roomTextFont() const -> const Font& { return room_text_font_; } [[nodiscard]] auto roomTextFont() const -> const Font& { return room_text_font_; }
[[nodiscard]] auto menuTitleFont() const -> const Font& { return menu_title_font_; } [[nodiscard]] auto menuTitleFont() const -> const Font& { return menu_title_font_; }
[[nodiscard]] auto menuItemsFont() const -> const Font& { return menu_items_font_; } [[nodiscard]] auto menuItemsFont() const -> const Font& { return menu_items_font_; }
[[nodiscard]] auto copyrightFont() const -> const Font& { return copyright_font_; }
// Geometria del Notifier. // Geometria del Notifier.
[[nodiscard]] auto notifier() const -> const Notifier& { return notifier_; } [[nodiscard]] auto notifier() const -> const Notifier& { return notifier_; }
@@ -173,6 +229,11 @@ class UiConfig {
Uint8 title_bg_color_; Uint8 title_bg_color_;
Uint8 title_text_color_; Uint8 title_text_color_;
Uint8 title_copyright_color_; Uint8 title_copyright_color_;
Uint8 title_press_start_outline_color_;
int title_logo_y_percent_;
int title_prompt_y_percent_;
int title_copyright_y_percent_;
int title_mini_logo_gap_px_;
int title_blink_visible_ms_; int title_blink_visible_ms_;
int title_blink_hidden_ms_; int title_blink_hidden_ms_;
int title_starting_ms_; int title_starting_ms_;
@@ -181,18 +242,22 @@ class UiConfig {
int title_fade_out_max_block_; int title_fade_out_max_block_;
int title_music_fade_out_ms_; int title_music_fade_out_ms_;
int title_attract_fade_to_black_ms_; int title_attract_fade_to_black_ms_;
int title_intro_fade_in_ms_; TitleIntro title_intro_;
TitleHeatHaze title_heat_haze_; TitleHeatHaze title_heat_haze_;
LogoScan logo_scan_;
LogoTimeline logo_timeline_;
std::string menu_select_sound_; std::string menu_select_sound_;
std::string menu_accept_sound_; std::string menu_accept_sound_;
std::string player_start_sound_; std::string player_start_sound_;
std::string logo_glitch_sound_;
Font hud_font_; Font hud_font_;
Font placeholder_font_; Font placeholder_font_;
Font room_text_font_; Font room_text_font_;
Font menu_title_font_; Font menu_title_font_;
Font menu_items_font_; Font menu_items_font_;
Font copyright_font_;
Notifier notifier_; Notifier notifier_;
+10 -10
View File
@@ -3,11 +3,11 @@
#include <exception> #include <exception>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace DebugConfig { namespace DebugConfig {
@@ -53,12 +53,12 @@ namespace DebugConfig {
if (!yaml.contains("start_scene")) { return StartScene::GAME; } if (!yaml.contains("start_scene")) { return StartScene::GAME; }
const auto RAW = yaml["start_scene"].get_value<std::string>(); const auto RAW = yaml["start_scene"].get_value<std::string>();
if (!isKnown(RAW)) { if (!isKnown(RAW)) {
std::cerr << "debug.yaml: start_scene \"" << RAW << "\" invàlid, arrancant a GAME\n"; Log::warn("debug.yaml", "start_scene \"{}\" invàlid, arrancant a GAME", RAW);
return StartScene::GAME; return StartScene::GAME;
} }
return fromName(RAW); return fromName(RAW);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "debug.yaml: error parsejant: " << e.what() << " — arrancant a GAME\n"; Log::error("debug.yaml", "error parsejant: {} — arrancant a GAME", e.what());
return StartScene::GAME; return StartScene::GAME;
} }
} }
@@ -72,7 +72,7 @@ namespace DebugConfig {
} }
const auto CONTENT = readFile(file_path); const auto CONTENT = readFile(file_path);
if (CONTENT.empty()) { if (CONTENT.empty()) {
std::cerr << "debug.yaml: fitxer buit a " << file_path << ", arrancant a GAME\n"; Log::warn("debug.yaml", "fitxer buit a {}, arrancant a GAME", file_path);
return StartScene::GAME; return StartScene::GAME;
} }
return parseStartScene(CONTENT); return parseStartScene(CONTENT);
@@ -85,12 +85,12 @@ namespace DebugConfig {
if (!yaml.contains("starting_players")) { return StartingPlayers::BOTH; } if (!yaml.contains("starting_players")) { return StartingPlayers::BOTH; }
const auto RAW = yaml["starting_players"].get_value<std::string>(); const auto RAW = yaml["starting_players"].get_value<std::string>();
if (!isKnownPlayers(RAW)) { if (!isKnownPlayers(RAW)) {
std::cerr << "debug.yaml: starting_players \"" << RAW << "\" invàlid, arrancant amb BOTH\n"; Log::warn("debug.yaml", "starting_players \"{}\" invàlid, arrancant amb BOTH", RAW);
return StartingPlayers::BOTH; return StartingPlayers::BOTH;
} }
return playersFromName(RAW); return playersFromName(RAW);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "debug.yaml: error parsejant starting_players: " << e.what() << " — arrancant amb BOTH\n"; Log::error("debug.yaml", "error parsejant starting_players: {} — arrancant amb BOTH", e.what());
return StartingPlayers::BOTH; return StartingPlayers::BOTH;
} }
} }
@@ -110,7 +110,7 @@ namespace DebugConfig {
if (!yaml.contains("starting_level")) { return std::nullopt; } if (!yaml.contains("starting_level")) { return std::nullopt; }
const auto& sl = yaml["starting_level"]; const auto& sl = yaml["starting_level"];
if (!sl.contains("pyramid") || !sl.contains("room")) { if (!sl.contains("pyramid") || !sl.contains("room")) {
std::cerr << "debug.yaml: starting_level necessita els camps `pyramid` i `room` — ignorat\n"; Log::warn("debug.yaml", "starting_level necessita els camps `pyramid` i `room` — ignorat");
return std::nullopt; return std::nullopt;
} }
return StartingLevel{ return StartingLevel{
@@ -118,7 +118,7 @@ namespace DebugConfig {
.room = sl["room"].get_value<int>(), .room = sl["room"].get_value<int>(),
}; };
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "debug.yaml: error parsejant starting_level: " << e.what() << " — ignorat\n"; Log::error("debug.yaml", "error parsejant starting_level: {} — ignorat", e.what());
return std::nullopt; return std::nullopt;
} }
} }
@@ -162,7 +162,7 @@ namespace DebugConfig {
if (std::filesystem::exists(file_path, error)) { return; } if (std::filesystem::exists(file_path, error)) { return; }
std::ofstream file(file_path); std::ofstream file(file_path);
if (!file) { if (!file) {
std::cerr << "debug.yaml: no s'ha pogut crear " << file_path << '\n'; Log::error("debug.yaml", "no s'ha pogut crear {}", file_path);
return; return;
} }
file << "# debug.yaml — ajust personal de dev (no versionat).\n"; file << "# debug.yaml — ajust personal de dev (no versionat).\n";
@@ -173,7 +173,7 @@ namespace DebugConfig {
file << "# clau, comença a la primera (P1-R1).\n"; file << "# clau, comença a la primera (P1-R1).\n";
file << "start_scene: GAME\n"; file << "start_scene: GAME\n";
file << "starting_players: BOTH\n"; file << "starting_players: BOTH\n";
std::cout << "debug.yaml: creat a " << file_path << " amb start_scene=GAME starting_players=BOTH\n"; Log::info("debug.yaml", "creat a {} amb start_scene=GAME starting_players=BOTH", file_path);
} }
} // namespace DebugConfig } // namespace DebugConfig
+1 -1
View File
@@ -5,7 +5,7 @@
#include <string> #include <string>
// Configuració personal de desenvolupament, carregada de `debug.yaml` dins la // Configuració personal de desenvolupament, carregada de `debug.yaml` dins la
// carpeta de sistema (`$XDG_CONFIG_HOME/jailgames/aee_2026/debug.yaml`). No es // carpeta de sistema (`$XDG_CONFIG_HOME/jailgames/aee_arcade/debug.yaml`). No es
// versiona: cada dev el crea a la seua màquina. Si no existeix, s'aplica el // versiona: cada dev el crea a la seua màquina. Si no existeix, s'aplica el
// valor per defecte (GAME) sense warning de nivell error. // valor per defecte (GAME) sense warning de nivell error.
namespace DebugConfig { namespace DebugConfig {
+1 -1
View File
@@ -234,7 +234,7 @@ void Editor::ensureEditableEntities() {
// llança si no troba — provem primer i, en cas de miss, carreguem. // llança si no troba — provem primer i, en cas de miss, carreguem.
const auto REL = sp["path"].get_value<std::string>(); const auto REL = sp["path"].get_value<std::string>();
const std::string KEY = std::filesystem::path(REL).filename().string(); const std::string KEY = std::filesystem::path(REL).filename().string();
std::string full = "data/entities/"; std::string full = Defaults::Paths::ENTITIES_DIR;
full += name; full += name;
full += '/'; full += '/';
full += REL; full += REL;
+5 -5
View File
@@ -3,13 +3,13 @@
#ifdef _DEBUG #ifdef _DEBUG
#include <cstdint> #include <cstdint>
#include <iostream>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -34,7 +34,7 @@ namespace {
if (!s.empty() && s.front() == '#') { s.erase(0, 1); } if (!s.empty() && s.front() == '#') { s.erase(0, 1); }
dst = static_cast<std::uint32_t>(std::stoul(s, nullptr, 0)); dst = static_cast<std::uint32_t>(std::stoul(s, nullptr, 0));
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "EditorConfig: color '" << key << "' invàlid (" << e.what() << ") — ignorat\n"; Log::warn("EditorConfig", "color '{}' invàlid ({}) — ignorat", key, e.what());
} }
} }
@@ -56,7 +56,7 @@ auto EditorConfig::get() -> EditorConfig& {
void EditorConfig::load(const std::string& file_path) { void EditorConfig::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path); auto bytes = Resource::Helper::loadFile(file_path);
if (bytes.empty()) { if (bytes.empty()) {
std::cerr << "EditorConfig: no s'ha pogut obrir " << file_path << " — s'usaran els defaults compilats\n"; Log::warn("EditorConfig", "no s'ha pogut obrir {} — s'usaran els defaults compilats", file_path);
return; return;
} }
try { try {
@@ -76,9 +76,9 @@ void EditorConfig::load(const std::string& file_path) {
readArgb(c, "picker_hover", picker_hover_argb_); readArgb(c, "picker_hover", picker_hover_argb_);
} }
std::cout << "EditorConfig: carregat des de " << file_path << '\n'; Log::info("EditorConfig", "carregat des de {}", file_path);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "EditorConfig: error parsejant " << file_path << ": " << e.what() << '\n'; Log::error("EditorConfig", "error parsejant {}: {}", file_path, e.what());
} }
} }
+1 -1
View File
@@ -6,7 +6,7 @@
#include <string> #include <string>
// Configuració visual de l'editor de tilemap (Debug-only). Carregada des // Configuració visual de l'editor de tilemap (Debug-only). Carregada des
// de data/editor/editor.yaml; les claus absents cauen als valors compilats // de data/config/editor.yaml; les claus absents cauen als valors compilats
// a Defaults::Editor. Mateix patró que UiConfig (Meyers singleton + load). // a Defaults::Editor. Mateix patró que UiConfig (Meyers singleton + load).
class EditorConfig { class EditorConfig {
public: public:
+14
View File
@@ -8,11 +8,14 @@
#include <string> #include <string>
#include <utility> #include <utility>
#include "core/audio/audio.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/entities/entity_factory.hpp" #include "game/entities/entity_factory.hpp"
#include "game/entities/entity_manager.hpp" #include "game/entities/entity_manager.hpp"
#include "game/entities/event_bus.hpp" #include "game/entities/event_bus.hpp"
#include "game/entities/player.hpp" #include "game/entities/player.hpp"
#include "game/map/map.hpp" #include "game/map/map.hpp"
#include "utils/defaults.hpp"
namespace { namespace {
@@ -133,6 +136,14 @@ Mummy::Mummy(const fkyaml::node& yaml)
if (h.contains("whippable")) { if (h.contains("whippable")) {
whippable_ = h["whippable"].get_value<bool>(); whippable_ = h["whippable"].get_value<bool>();
} }
// So opcional: el basename val tant de clau al Resource::Cache com de
// path (`Defaults::Paths::AUDIO_SOUNDS_DIR + key`). Mateix patró que
// els sons del Player. Si el camp manca, la clau queda buida i
// `enterHurt` no dispara playSound.
if (h.contains("sound")) {
hurt_config_.sound_key = h["sound"].get_value<std::string>();
Resource::Cache::get().loadSound(hurt_config_.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + hurt_config_.sound_key);
}
} }
} }
@@ -511,6 +522,9 @@ void Mummy::enterHurt(Direction flee_dir) {
state_ = State::HURT; state_ = State::HURT;
hurt_remaining_s_ = hurt_config_.duration_s; hurt_remaining_s_ = hurt_config_.duration_s;
hurt_speed_ = (hurt_config_.speed_px_per_s > 0.0F) ? hurt_config_.speed_px_per_s : speed_; hurt_speed_ = (hurt_config_.speed_px_per_s > 0.0F) ? hurt_config_.speed_px_per_s : speed_;
if (!hurt_config_.sound_key.empty() && Audio::get() != nullptr) {
Audio::get()->playSound(hurt_config_.sound_key, Audio::Group::GAME);
}
const Direction CHOSEN = chooseHurtDirection(flee_dir); const Direction CHOSEN = chooseHurtDirection(flee_dir);
if (CHOSEN == Direction::NONE) { if (CHOSEN == Direction::NONE) {
+2
View File
@@ -3,6 +3,7 @@
#include <climits> #include <climits>
#include <cstdint> #include <cstdint>
#include <optional> #include <optional>
#include <string>
#include <vector> #include <vector>
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
@@ -35,6 +36,7 @@ class Mummy : public DynamicHazard {
struct HurtConfig { struct HurtConfig {
float duration_s{0.0F}; float duration_s{0.0F};
float speed_px_per_s{0.0F}; float speed_px_per_s{0.0F};
std::string sound_key; // Buit = sense so. El fitxer viu a Defaults::Paths::AUDIO_SOUNDS_DIR.
}; };
explicit Mummy(const fkyaml::node& yaml); explicit Mummy(const fkyaml::node& yaml);
+2 -1
View File
@@ -6,6 +6,7 @@
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "game/entities/entity.hpp" #include "game/entities/entity.hpp"
#include "game/entities/entity_factory.hpp" #include "game/entities/entity_factory.hpp"
#include "utils/defaults.hpp"
namespace EntityLoader { namespace EntityLoader {
@@ -20,7 +21,7 @@ namespace EntityLoader {
} }
auto templatePath(const std::string& name) -> std::string { auto templatePath(const std::string& name) -> std::string {
return "data/entities/" + name + "/" + name + ".yaml"; return std::string{Defaults::Paths::ENTITIES_DIR} + name + "/" + name + ".yaml";
} }
} // namespace } // namespace
@@ -5,7 +5,6 @@
// stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer. // stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer.
#include <filesystem> #include <filesystem>
#include <iostream>
#include <memory> #include <memory>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -21,6 +20,8 @@
#include "game/entities/entity_manager.hpp" #include "game/entities/entity_manager.hpp"
#include "game/entities/player.hpp" #include "game/entities/player.hpp"
#include "game/map/map.hpp" #include "game/map/map.hpp"
#include "utils/defaults.hpp"
#include "utils/log.hpp"
// Render centralitzat al manager per confinar la dependència cap a // Render centralitzat al manager per confinar la dependència cap a
// `Surface`. Les entitats exposen `getSurface()` + `getPixelRect()` i el // `Surface`. Les entitats exposen `getSurface()` + `getPixelRect()` i el
@@ -124,7 +125,7 @@ namespace {
if (s.contains("path")) { if (s.contains("path")) {
const auto REL = s["path"].get_value<std::string>(); const auto REL = s["path"].get_value<std::string>();
info.key = std::filesystem::path(REL).filename().string(); info.key = std::filesystem::path(REL).filename().string();
info.full_path = "data/entities/" + type + "/" + REL; info.full_path = std::string{Defaults::Paths::ENTITIES_DIR} + type + "/" + REL;
info.valid = true; info.valid = true;
} }
if (s.contains("size")) { if (s.contains("size")) {
@@ -236,12 +237,12 @@ namespace {
try { try {
tmpl = EntityLoader::loadTemplate(type); tmpl = EntityLoader::loadTemplate(type);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "EntityManager: " << e.what() << '\n'; Log::error("EntityManager", "{}", e.what());
return nullptr; return nullptr;
} }
auto entity = EntityFactory::instance().create(type, tmpl); auto entity = EntityFactory::instance().create(type, tmpl);
if (entity == nullptr) { if (entity == nullptr) {
std::cerr << "EntityManager: tipus desconegut '" << type << "'\n"; Log::error("EntityManager", "tipus desconegut '{}'", type);
return nullptr; return nullptr;
} }
sprite_out = readSpriteInfo(tmpl, type); sprite_out = readSpriteInfo(tmpl, type);
@@ -266,7 +267,7 @@ namespace {
namespace { namespace {
auto placeSarcophagus(EntityManager& manager, std::unique_ptr<Entity> entity, const SpriteInfo& sprite, const fkyaml::node& spawn) -> bool { auto placeSarcophagus(EntityManager& manager, std::unique_ptr<Entity> entity, const SpriteInfo& sprite, const fkyaml::node& spawn) -> bool {
if (!spawn.contains("sarc_position")) { if (!spawn.contains("sarc_position")) {
std::cerr << "EntityManager: sarcophagus sense sarc_position\n"; Log::error("EntityManager", "sarcophagus sense sarc_position");
return false; return false;
} }
const auto& sp = spawn["sarc_position"]; const auto& sp = spawn["sarc_position"];
@@ -303,7 +304,7 @@ namespace {
// un `Sarcophagus`. Usant el retorn de `release` al ctor del nou // un `Sarcophagus`. Usant el retorn de `release` al ctor del nou
// `unique_ptr` evitem el lint `bugprone-unused-return-value`. // `unique_ptr` evitem el lint `bugprone-unused-return-value`.
if (dynamic_cast<Sarcophagus*>(entity.get()) == nullptr) { if (dynamic_cast<Sarcophagus*>(entity.get()) == nullptr) {
std::cerr << "EntityManager: la factory no ha creat un Sarcophagus\n"; Log::error("EntityManager", "la factory no ha creat un Sarcophagus");
return false; return false;
} }
std::unique_ptr<Sarcophagus> sarc(static_cast<Sarcophagus*>(entity.release())); std::unique_ptr<Sarcophagus> sarc(static_cast<Sarcophagus*>(entity.release()));
@@ -322,7 +323,7 @@ namespace {
// queden tapats — comportament intencional. // queden tapats — comportament intencional.
auto placeDoor(EntityManager& manager, std::unique_ptr<Entity> entity, const SpriteInfo& sprite) -> bool { auto placeDoor(EntityManager& manager, std::unique_ptr<Entity> entity, const SpriteInfo& sprite) -> bool {
if (Map::door_col <= 0 || Map::tile_size <= 0) { if (Map::door_col <= 0 || Map::tile_size <= 0) {
std::cerr << "EntityManager: door requereix `geometry.door_col` definit al mapa\n"; Log::error("EntityManager", "door requereix `geometry.door_col` definit al mapa");
return false; return false;
} }
const float CENTER_X = static_cast<float>((Map::door_col * Map::tile_size) + Map::tilemap_origin_x) + (static_cast<float>(Map::tile_size) / 2.0F); const float CENTER_X = static_cast<float>((Map::door_col * Map::tile_size) + Map::tilemap_origin_x) + (static_cast<float>(Map::tile_size) / 2.0F);
@@ -342,7 +343,7 @@ namespace {
auto placeGeneric(EntityManager& manager, std::unique_ptr<Entity> entity, const std::string& type, const SpriteInfo& sprite, const std::string& anchor, const fkyaml::node& spawn) -> bool { auto placeGeneric(EntityManager& manager, std::unique_ptr<Entity> entity, const std::string& type, const SpriteInfo& sprite, const std::string& anchor, const fkyaml::node& spawn) -> bool {
if (!spawn.contains("position")) { if (!spawn.contains("position")) {
std::cerr << "EntityManager: " << type << " sense position\n"; Log::error("EntityManager", "{} sense position", type);
return false; return false;
} }
const auto& p = spawn["position"]; const auto& p = spawn["position"];
@@ -443,10 +444,8 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
++decal_broadcast; ++decal_broadcast;
} }
if (decal_broadcast > 0) { if (decal_broadcast > 0) {
std::cout << "EntityManager: " << decal_broadcast Log::info("EntityManager", "{} tile_painted broadcast inicial (decals de sòl)", decal_broadcast);
<< " tile_painted broadcast inicial (decals de sòl)\n";
} }
std::cout << "EntityManager: " << LOADED << " entitats carregades des de " Log::info("EntityManager", "{} entitats carregades des de {}", LOADED, level_yaml_path);
<< level_yaml_path << '\n';
} }
+10 -9
View File
@@ -179,16 +179,16 @@ void Player::readJumpBlock(const fkyaml::node& yaml) {
if (j.contains("gravity")) { jump_config.gravity_px_per_s2 = j["gravity"].get_value<float>(); } if (j.contains("gravity")) { jump_config.gravity_px_per_s2 = j["gravity"].get_value<float>(); }
if (j.contains("landing_stun_ms")) { jump_config.landing_stun_ms = j["landing_stun_ms"].get_value<int>(); } if (j.contains("landing_stun_ms")) { jump_config.landing_stun_ms = j["landing_stun_ms"].get_value<int>(); }
if (j.contains("shadow_y_offset")) { jump_config.shadow_y_offset = j["shadow_y_offset"].get_value<int>(); } if (j.contains("shadow_y_offset")) { jump_config.shadow_y_offset = j["shadow_y_offset"].get_value<int>(); }
// Sons opcionals: el nom del fitxer (relatiu a data/sounds/) val tant // Sons opcionals: el nom del fitxer (relatiu a Defaults::Paths::AUDIO_SOUNDS_DIR)
// de clau al Resource::Cache com de basename per a la càrrega. Si el // val tant de clau al Resource::Cache com de basename per a la càrrega.
// camp manca, la clau queda buida i `playSound` no es crida. // Si el camp manca, la clau queda buida i `playSound` no es crida.
if (j.contains("sound")) { if (j.contains("sound")) {
jump_config.sound_key = j["sound"].get_value<std::string>(); jump_config.sound_key = j["sound"].get_value<std::string>();
Resource::Cache::get().loadSound(jump_config.sound_key, "data/sounds/" + jump_config.sound_key); Resource::Cache::get().loadSound(jump_config.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + jump_config.sound_key);
} }
if (j.contains("land_sound")) { if (j.contains("land_sound")) {
jump_config.land_sound_key = j["land_sound"].get_value<std::string>(); jump_config.land_sound_key = j["land_sound"].get_value<std::string>();
Resource::Cache::get().loadSound(jump_config.land_sound_key, "data/sounds/" + jump_config.land_sound_key); Resource::Cache::get().loadSound(jump_config.land_sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + jump_config.land_sound_key);
} }
} }
@@ -201,7 +201,7 @@ void Player::readHurtBlock(const fkyaml::node& yaml) {
// So d'entrada a HURT — mateix patró que `jump.sound` i `whip.sound`. // So d'entrada a HURT — mateix patró que `jump.sound` i `whip.sound`.
if (h.contains("sound")) { if (h.contains("sound")) {
hurt_config.sound_key = h["sound"].get_value<std::string>(); hurt_config.sound_key = h["sound"].get_value<std::string>();
Resource::Cache::get().loadSound(hurt_config.sound_key, "data/sounds/" + hurt_config.sound_key); Resource::Cache::get().loadSound(hurt_config.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + hurt_config.sound_key);
} }
} }
@@ -220,10 +220,11 @@ void Player::readImmunityBlock(const fkyaml::node& yaml) {
if (im.contains("duration_ms")) { immunity_config.duration_ms = im["duration_ms"].get_value<float>(); } if (im.contains("duration_ms")) { immunity_config.duration_ms = im["duration_ms"].get_value<float>(); }
if (im.contains("blink_period_ms")) { immunity_config.blink_period_ms = im["blink_period_ms"].get_value<int>(); } if (im.contains("blink_period_ms")) { immunity_config.blink_period_ms = im["blink_period_ms"].get_value<int>(); }
// Paleta del blink: el basename val tant de clau al Resource::Cache // Paleta del blink: el basename val tant de clau al Resource::Cache
// com de path (`data/player/<key>.pal`). Mateix patró que els sons. // com de path (`Defaults::Paths::PLAYER_DIR + key + .pal`). Mateix patró
// que els sons.
if (im.contains("palette")) { if (im.contains("palette")) {
immunity_config.palette_key = im["palette"].get_value<std::string>(); immunity_config.palette_key = im["palette"].get_value<std::string>();
Resource::Cache::get().loadPalette(immunity_config.palette_key, "data/player/" + immunity_config.palette_key + ".pal"); Resource::Cache::get().loadPalette(immunity_config.palette_key, std::string{Defaults::Paths::PLAYER_DIR} + immunity_config.palette_key + ".pal");
} }
} }
@@ -239,7 +240,7 @@ void Player::readWhipBlock(const fkyaml::node& yaml) {
// clau al cache i de path. Si manca, queda buit i no es dispara. // clau al cache i de path. Si manca, queda buit i no es dispara.
if (w.contains("sound")) { if (w.contains("sound")) {
whip_config.sound_key = w["sound"].get_value<std::string>(); whip_config.sound_key = w["sound"].get_value<std::string>();
Resource::Cache::get().loadSound(whip_config.sound_key, "data/sounds/" + whip_config.sound_key); Resource::Cache::get().loadSound(whip_config.sound_key, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + whip_config.sound_key);
} }
} }
+7 -7
View File
@@ -91,7 +91,7 @@ class Player : public AnimatedSprite {
DYING, DYING,
}; };
// Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`). // Geometria carregada des del YAML del player (p.ex. `data/entities/player/sam.yaml`).
// `sprite_*` són // `sprite_*` són
// la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els // la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els
// 12 píxels superiors són el cap i els 12 inferiors la base). // 12 píxels superiors són el cap i els 12 inferiors la base).
@@ -362,7 +362,7 @@ class Player : public AnimatedSprite {
// al respawn post-mort: mateixa entrada cinemàtica. // al respawn post-mort: mateixa entrada cinemàtica.
void spawnFromAir(float x, float y); void spawnFromAir(float x, float y);
// Arma un preset de reverb (catàleg a `data/sounds.yaml`) per al // Arma un preset de reverb (catàleg a `data/config/sounds.yaml`) per al
// pròxim aterratge d'aquest jugador. Pensat per a la cinemàtica // pròxim aterratge d'aquest jugador. Pensat per a la cinemàtica
// d'INTRO de sala: GameScene crida aquesta funció just després de // d'INTRO de sala: GameScene crida aquesta funció just després de
// `spawnFromAir`, i el primer LAND consumeix el preset (queda buit // `spawnFromAir`, i el primer LAND consumeix el preset (queda buit
@@ -529,7 +529,7 @@ class Player : public AnimatedSprite {
// gestiona `intended_vx_/vy_` escalats pel resolver. // gestiona `intended_vx_/vy_` escalats pel resolver.
bool being_pushed_{false}; bool being_pushed_{false};
// Preset de reverb (catàleg de `data/sounds.yaml`) que el pròxim // Preset de reverb (catàleg de `data/config/sounds.yaml`) que el pròxim
// LAND ha de fer servir; buit = so sec. L'arma `armNextLandReverb` // LAND ha de fer servir; buit = so sec. L'arma `armNextLandReverb`
// (cridada per GameScene al spawn aeri d'INTRO) i el consumeix // (cridada per GameScene al spawn aeri d'INTRO) i el consumeix
// `integrateJump` quan el jugador toca terra. Cap altre punt // `integrateJump` quan el jugador toca terra. Cap altre punt
@@ -636,7 +636,7 @@ class Player : public AnimatedSprite {
struct HurtConfig { struct HurtConfig {
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s). float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
int brake_time_ms{0}; // Temps en que la velocitat decau a 0. int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
// Fitxer .wav reproduït en entrar a HURT (relatiu a `data/sounds/`). // Fitxer .wav reproduït en entrar a HURT (relatiu a `data/audio/sounds/`).
// Mateixa convenció que la resta de sons del jugador: el basename val // Mateixa convenció que la resta de sons del jugador: el basename val
// de clau al Resource::Cache i de path. Buit o absent = entrada // de clau al Resource::Cache i de path. Buit o absent = entrada
// silenciosa. // silenciosa.
@@ -660,7 +660,7 @@ class Player : public AnimatedSprite {
int cooldown_ms{0}; int cooldown_ms{0};
int damage{0}; int damage{0};
// Fitxer .wav reproduït al moment de disparar el swing (relatiu a // Fitxer .wav reproduït al moment de disparar el swing (relatiu a
// `data/sounds/`). Convenció: el basename val tant de clau al // `data/audio/sounds/`). Convenció: el basename val tant de clau al
// Resource::Cache com de path. Buit o absent = swing silenciós. // Resource::Cache com de path. Buit o absent = swing silenciós.
std::string sound_key; std::string sound_key;
}; };
@@ -697,7 +697,7 @@ class Player : public AnimatedSprite {
float duration_ms{0.0F}; float duration_ms{0.0F};
int blink_period_ms{0}; int blink_period_ms{0};
// Clau al Resource::Cache de la paleta plana usada al blink (basename // Clau al Resource::Cache de la paleta plana usada al blink (basename
// del .pal a `data/player/`). Buida = no hi ha alternança visual, // del .pal a `data/entities/player/`). Buida = no hi ha alternança visual,
// només la finestra lògica d'immunitat (que sense feedback visual // només la finestra lògica d'immunitat (que sense feedback visual
// pot ser confusa, però no és incorrecte). // pot ser confusa, però no és incorrecte).
std::string palette_key; std::string palette_key;
@@ -706,7 +706,7 @@ class Player : public AnimatedSprite {
// Llig el bloc `immunity:` del YAML i n'omple `immunity_config`. Si // Llig el bloc `immunity:` del YAML i n'omple `immunity_config`. Si
// hi ha `palette`, registra la paleta corresponent al Resource::Cache // hi ha `palette`, registra la paleta corresponent al Resource::Cache
// (path: `data/player/<palette_key>.pal`). Mateix patró que els altres // (path: `data/entities/player/<palette_key>.pal`). Mateix patró que els altres
// readers — `loadFrom` el crida al final. // readers — `loadFrom` el crida al final.
static void readImmunityBlock(const fkyaml::node& yaml); static void readImmunityBlock(const fkyaml::node& yaml);
+7 -13
View File
@@ -1,13 +1,13 @@
#include "game/levels/level_manager.hpp" #include "game/levels/level_manager.hpp"
#include <algorithm> #include <algorithm>
#include <iostream>
#include <stdexcept> #include <stdexcept>
#include <utility> #include <utility>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/log.hpp"
namespace Levels { namespace Levels {
@@ -46,8 +46,7 @@ namespace Levels {
throw std::runtime_error("LevelManager: la seqüència de " + PATH + " és buida"); throw std::runtime_error("LevelManager: la seqüència de " + PATH + " és buida");
} }
index_ = 0; index_ = 0;
std::cout << "LevelManager: " << entries_.size() << " sales carregades de " Log::info("LevelManager", "{} sales carregades de {} (començant a {})", entries_.size(), PATH, currentLabel());
<< PATH << " (començant a " << currentLabel() << ")\n";
} }
auto LevelManager::currentPath() const -> const std::string& { auto LevelManager::currentPath() const -> const std::string& {
@@ -67,22 +66,20 @@ namespace Levels {
auto LevelManager::advance() -> bool { auto LevelManager::advance() -> bool {
if (index_ + 1 >= count()) { return false; } if (index_ + 1 >= count()) { return false; }
++index_; ++index_;
std::cout << "LevelManager: avança a " << currentLabel() Log::info("LevelManager", "avança a {} ({})", currentLabel(), currentPath());
<< " (" << currentPath() << ")\n";
return true; return true;
} }
void LevelManager::restart() { void LevelManager::restart() {
index_ = 0; index_ = 0;
std::cout << "LevelManager: restart a " << currentLabel() << '\n'; Log::info("LevelManager", "restart a {}", currentLabel());
} }
void LevelManager::setIndex(int new_index) { void LevelManager::setIndex(int new_index) {
const int CLAMPED = std::clamp(new_index, 0, count() - 1); const int CLAMPED = std::clamp(new_index, 0, count() - 1);
if (CLAMPED != index_) { if (CLAMPED != index_) {
index_ = CLAMPED; index_ = CLAMPED;
std::cout << "LevelManager: setIndex(" << new_index << ") → " Log::info("LevelManager", "setIndex({}) → {} ({})", new_index, currentLabel(), currentPath());
<< currentLabel() << " (" << currentPath() << ")\n";
} }
} }
@@ -90,14 +87,11 @@ namespace Levels {
for (std::size_t i = 0; i < entries_.size(); ++i) { for (std::size_t i = 0; i < entries_.size(); ++i) {
if (entries_[i].pyramid == pyramid && entries_[i].room == room) { if (entries_[i].pyramid == pyramid && entries_[i].room == room) {
index_ = static_cast<int>(i); index_ = static_cast<int>(i);
std::cout << "LevelManager: starting_point P" << pyramid << "-R" << room Log::info("LevelManager", "starting_point P{}-R{} trobat a l'índex {} ({})", pyramid, room, i, currentPath());
<< " trobat a l'índex " << i << " (" << currentPath() << ")\n";
return true; return true;
} }
} }
std::cerr << "LevelManager: starting_point P" << pyramid << "-R" << room Log::warn("LevelManager", "starting_point P{}-R{} no trobat a levels.yaml — es manté l'índex actual ({})", pyramid, room, currentLabel());
<< " no trobat a levels.yaml — es manté l'índex actual ("
<< currentLabel() << ")\n";
return false; return false;
} }
+2 -6
View File
@@ -1,11 +1,11 @@
#include "game/map/map.hpp" #include "game/map/map.hpp"
#include <cmath> #include <cmath>
#include <iostream>
#include <stdexcept> #include <stdexcept>
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -90,11 +90,7 @@ namespace Map {
// canviarà el flag a true quan transiti a OPEN. // canviarà el flag a true quan transiti a OPEN.
door_open = false; door_open = false;
std::cout << "Map: geometria " << grid_cols << "x" << grid_rows Log::info("Map", "geometria {}x{} sarcs {}x{} pitch {}x{} door_col {} des de {}", grid_cols, grid_rows, sarc_w, sarc_h, pitch_x, pitch_y, door_col, yaml_path);
<< " sarcs " << sarc_w << "x" << sarc_h
<< " pitch " << pitch_x << "x" << pitch_y
<< " door_col " << door_col
<< " des de " << yaml_path << '\n';
} }
auto getSarcRect(int col, int row) -> SDL_FRect { auto getSarcRect(int col, int row) -> SDL_FRect {
+2 -4
View File
@@ -3,7 +3,6 @@
#include <algorithm> #include <algorithm>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <iostream>
#include <variant> #include <variant>
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
@@ -11,6 +10,7 @@
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "game/map/map.hpp" #include "game/map/map.hpp"
#include "game/map/tilemap.hpp" #include "game/map/tilemap.hpp"
#include "utils/log.hpp"
// CONTRACTE BUS DE PINTAT // CONTRACTE BUS DE PINTAT
// ----------------------- // -----------------------
@@ -89,9 +89,7 @@ PaintLayer::PaintLayer(const Tilemap& tilemap, EventBus* bus)
}); });
} }
std::cout << "PaintLayer: " << cols_ << "x" << rows_ << " tiles (" Log::info("PaintLayer", "{}x{} tiles ({}x{} px), tileset {}", cols_, rows_, width_px_, height_px_, tileset_path_);
<< width_px_ << "x" << height_px_ << " px), tileset "
<< tileset_path_ << '\n';
} }
PaintLayer::~PaintLayer() { PaintLayer::~PaintLayer() {
+2 -3
View File
@@ -3,7 +3,6 @@
#include <algorithm> #include <algorithm>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <iostream>
#include <stdexcept> #include <stdexcept>
#include <vector> #include <vector>
@@ -12,6 +11,7 @@
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
#ifdef _DEBUG #ifdef _DEBUG
#include "game/map/tilemap_writer.hpp" #include "game/map/tilemap_writer.hpp"
@@ -97,8 +97,7 @@ Tilemap::Tilemap(const std::string& yaml_path) {
} }
} }
std::cout << "Tilemap: " << cols_ << "x" << rows_ << " tiles (" Log::info("Tilemap", "{}x{} tiles ({}x{} px) des de {}", cols_, rows_, width_px_, height_px_, yaml_path);
<< width_px_ << "x" << height_px_ << " px) des de " << yaml_path << '\n';
} }
#ifdef _DEBUG #ifdef _DEBUG
+14 -15
View File
@@ -3,7 +3,6 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <fstream> #include <fstream>
#include <iostream>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -12,6 +11,7 @@
#include "core/input/gamepad_manager.hpp" #include "core/input/gamepad_manager.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -104,7 +104,7 @@ namespace {
std::ofstream dst(path + ".bak", std::ios::binary); std::ofstream dst(path + ".bak", std::ios::binary);
if (!dst) { return; } if (!dst) { return; }
dst << src.rdbuf(); dst << src.rdbuf();
std::cout << "Options: còpia de seguretat a " << path << ".bak\n"; Log::info("Options", "còpia de seguretat a {}.bak", path);
} }
void parseWindow(const fkyaml::node& yaml) { void parseWindow(const fkyaml::node& yaml) {
@@ -463,7 +463,7 @@ namespace {
try { try {
auto yaml = fkyaml::node::deserialize(CONTENT); auto yaml = fkyaml::node::deserialize(CONTENT);
if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) { if (!yaml.contains("presets") || !yaml["presets"].is_sequence()) {
std::cout << "Options: " << kind_label << " manca `presets:`; reescrivint amb defaults.\n"; Log::warn("Options", "{} manca `presets:`; reescrivint amb defaults.", kind_label);
backupFile(path); backupFile(path);
presets = defaults_fn(); presets = defaults_fn();
return save_fn(); return save_fn();
@@ -475,15 +475,15 @@ namespace {
presets.push_back(preset); presets.push_back(preset);
} }
if (presets.empty()) { if (presets.empty()) {
std::cout << "Options: " << kind_label << " sense presets; reescrivint amb defaults.\n"; Log::warn("Options", "{} sense presets; reescrivint amb defaults.", kind_label);
backupFile(path); backupFile(path);
presets = defaults_fn(); presets = defaults_fn();
return save_fn(); return save_fn();
} }
return true; return true;
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cout << "Options: error parsejant " << kind_label << ": " << e.what() << '\n'; Log::error("Options", "error parsejant {}: {}", kind_label, e.what());
std::cout << "Options: reescrivint el fitxer amb presets per defecte.\n"; Log::info("Options", "reescrivint el fitxer amb presets per defecte.");
backupFile(path); backupFile(path);
presets = defaults_fn(); presets = defaults_fn();
return save_fn(); return save_fn();
@@ -655,8 +655,7 @@ namespace Options {
// amb els defaults de la versió nova. El fitxer original queda // amb els defaults de la versió nova. El fitxer original queda
// a `<path>.bak` per si l'usuari volia preservar tuning manual. // a `<path>.bak` per si l'usuari volia preservar tuning manual.
if (file_version != Texts::VERSION) { if (file_version != Texts::VERSION) {
std::cout << "Options: config.yaml versió \"" << file_version Log::warn("Options", "config.yaml versió \"{}\" != \"{}\"; reescrivint amb defaults.", file_version, Texts::VERSION);
<< "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n";
backupFile(g_config_file_path); backupFile(g_config_file_path);
return saveToFile(); return saveToFile();
} }
@@ -679,8 +678,8 @@ namespace Options {
if (CHANGED) { saveToFile(); } if (CHANGED) { saveToFile(); }
return true; return true;
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n'; Log::error("Options", "error parsejant config.yaml: {}", e.what());
std::cout << "Options: reescrivint el fitxer amb defaults.\n"; Log::info("Options", "reescrivint el fitxer amb defaults.");
backupFile(g_config_file_path); backupFile(g_config_file_path);
return saveToFile(); return saveToFile();
} }
@@ -692,7 +691,7 @@ namespace Options {
g_config_version = Texts::VERSION; g_config_version = Texts::VERSION;
std::ostringstream out; std::ostringstream out;
out << "# Configuració d'AEE 2026. Generat automàticament; editable a mà.\n"; out << "# Configuració d'AEE ARCADE. Generat automàticament; editable a mà.\n";
out << "version: \"" << g_config_version << "\"\n"; out << "version: \"" << g_config_version << "\"\n";
out << "locale: \"" << g_locale << "\"\n\n"; out << "locale: \"" << g_locale << "\"\n\n";
@@ -780,7 +779,7 @@ namespace Options {
g_postfx_file_path, g_postfx_file_path,
g_postfx_presets, g_postfx_presets,
&writePostFXPreset, &writePostFXPreset,
"Presets PostFX per a AEE 2026. Editable a mà.", "Presets PostFX per a AEE ARCADE. Editable a mà.",
"Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset."); "Els noms han de coincidir amb config.yaml > video.shader.current_postfx_preset.");
} }
@@ -803,7 +802,7 @@ namespace Options {
g_crtpi_file_path, g_crtpi_file_path,
g_crtpi_presets, g_crtpi_presets,
&writeCrtPiPreset, &writeCrtPiPreset,
"Presets CRT-Pi per a AEE 2026. Editable a mà.", "Presets CRT-Pi per a AEE ARCADE. Editable a mà.",
"Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset."); "Els noms han de coincidir amb config.yaml > video.shader.current_crtpi_preset.");
} }
@@ -843,7 +842,7 @@ namespace Options {
const auto IT = std::ranges::find_if(g_crtpi_presets, const auto IT = std::ranges::find_if(g_crtpi_presets,
[](const CrtPiPreset& preset) { return preset.name == g_video.shader.current_crtpi_preset; }); [](const CrtPiPreset& preset) { return preset.name == g_video.shader.current_crtpi_preset; });
if (IT == g_crtpi_presets.end()) { if (IT == g_crtpi_presets.end()) {
std::cout << "Options: preset CrtPi \"" << g_video.shader.current_crtpi_preset << "\" no trobat; aplicant defaults.\n"; Log::warn("Options", "preset CrtPi \"{}\" no trobat; aplicant defaults.", g_video.shader.current_crtpi_preset);
return {}; return {};
} }
return { return {
@@ -871,7 +870,7 @@ namespace Options {
const auto IT = std::ranges::find_if(g_postfx_presets, const auto IT = std::ranges::find_if(g_postfx_presets,
[](const PostFXPreset& preset) { return preset.name == g_video.shader.current_postfx_preset; }); [](const PostFXPreset& preset) { return preset.name == g_video.shader.current_postfx_preset; });
if (IT == g_postfx_presets.end()) { if (IT == g_postfx_presets.end()) {
std::cout << "Options: preset PostFX \"" << g_video.shader.current_postfx_preset << "\" no trobat; aplicant passthrough.\n"; Log::warn("Options", "preset PostFX \"{}\" no trobat; aplicant passthrough.", g_video.shader.current_postfx_preset);
return {}; return {};
} }
return { return {
+30 -34
View File
@@ -7,7 +7,6 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <iostream>
#include <iterator> #include <iterator>
#include <utility> #include <utility>
#include <variant> #include <variant>
@@ -41,6 +40,7 @@
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
#include "utils/easing.hpp" #include "utils/easing.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -229,10 +229,10 @@ auto GameScene::defaultConfig() -> Config {
// concret de sala — només cap a on apunta la progressió ara mateix. // concret de sala — només cap a on apunta la progressió ara mateix.
return { return {
.map_yaml = Levels::LevelManager::get().currentPath(), .map_yaml = Levels::LevelManager::get().currentPath(),
.player_sprite = Defaults::Pyramid::PLAYER_SPRITE, .player_sprite = Defaults::Pyramid::PLAYER_SAM_GIF,
.player_yaml = Defaults::Pyramid::PLAYER_YAML, .player_yaml = Defaults::Pyramid::PLAYER_SAM_YAML,
.player2_sprite = Defaults::Pyramid::PLAYER_SPRITE_P2, .player2_sprite = Defaults::Pyramid::PLAYER_MORCUS_GIF,
.player2_yaml = Defaults::Pyramid::PLAYER_YAML_P2, .player2_yaml = Defaults::Pyramid::PLAYER_MORCUS_YAML,
.music_track = Defaults::Pyramid::MUSIC_TRACK, .music_track = Defaults::Pyramid::MUSIC_TRACK,
.starting_players = DebugConfig::StartingPlayers::BOTH, .starting_players = DebugConfig::StartingPlayers::BOTH,
}; };
@@ -290,13 +290,12 @@ GameScene::GameScene(Config config)
// re-emissions es filtren per estat per no re-entrar mai des d'una // re-emissions es filtren per estat per no re-entrar mai des d'una
// transició ja en marxa. // transició ja en marxa.
entity_manager_.getBus().subscribe("level_complete", [this](const Event& ev) { entity_manager_.getBus().subscribe("level_complete", [this](const Event& ev) {
std::cout << "[LEVEL COMPLETE] player_id=" << ev.player_id << '\n'; Log::info("LEVEL COMPLETE", "player_id={}", ev.player_id);
if (state_ == RoomFsm::State::PLAYING) { setState(RoomFsm::State::ROOM_OUTRO); } if (state_ == RoomFsm::State::PLAYING) { setState(RoomFsm::State::ROOM_OUTRO); }
}); });
entity_manager_.getBus().subscribe("player_hit", [](const Event& ev) { entity_manager_.getBus().subscribe("player_hit", [](const Event& ev) {
const auto* dmg = std::get_if<int>(&ev.payload); const auto* dmg = std::get_if<int>(&ev.payload);
std::cout << "[PLAYER HIT] player_id=" << ev.player_id Log::info("PLAYER HIT", "player_id={} dmg={}", ev.player_id, dmg != nullptr ? *dmg : 0);
<< " dmg=" << (dmg != nullptr ? *dmg : 0) << '\n';
}); });
// `player_killed` el publica un hazard letal (mòmia amb // `player_killed` el publica un hazard letal (mòmia amb
// `kills_outright`) després del `player_hit`. Aquí resolem el // `kills_outright`) després del `player_hit`. Aquí resolem el
@@ -312,7 +311,7 @@ GameScene::GameScene(Config config)
// També ens subscrivim a `player_jumped` i `player_landed` per // També ens subscrivim a `player_jumped` i `player_landed` per
// spawnejar el polvet de despegue / aterratge a la posició dels // spawnejar el polvet de despegue / aterratge a la posició dels
// peus que el Player ens dóna al payload. // peus que el Player ens dóna al payload.
Resource::Cache::get().loadSurface("dust.gif", "data/entities/dust/dust.gif"); Resource::Cache::get().loadSurface("dust.gif", Defaults::Paths::DUST_GIF);
entity_manager_.getBus().subscribe("player_jumped", [this](const Event& ev) { entity_manager_.getBus().subscribe("player_jumped", [this](const Event& ev) {
const auto* pos = std::get_if<WorldPos>(&ev.payload); const auto* pos = std::get_if<WorldPos>(&ev.payload);
if (pos != nullptr) { spawnDust(pos->x, pos->y, false); } if (pos != nullptr) { spawnDust(pos->x, pos->y, false); }
@@ -325,10 +324,10 @@ GameScene::GameScene(Config config)
// Whip: precarreguem les 4 surfaces (una per cardinal) abans del // Whip: precarreguem les 4 surfaces (una per cardinal) abans del
// primer spawn perquè el Player no facte disc al primer atac. // primer spawn perquè el Player no facte disc al primer atac.
// Subscriu `whip_spawn` per crear el `WhipAttack` corresponent. // Subscriu `whip_spawn` per crear el `WhipAttack` corresponent.
Resource::Cache::get().loadSurface("whip_east.gif", "data/entities/whip_attack/whip_east.gif"); Resource::Cache::get().loadSurface("whip_east.gif", Defaults::Paths::WHIP_EAST_GIF);
Resource::Cache::get().loadSurface("whip_west.gif", "data/entities/whip_attack/whip_west.gif"); Resource::Cache::get().loadSurface("whip_west.gif", Defaults::Paths::WHIP_WEST_GIF);
Resource::Cache::get().loadSurface("whip_north.gif", "data/entities/whip_attack/whip_north.gif"); Resource::Cache::get().loadSurface("whip_north.gif", Defaults::Paths::WHIP_NORTH_GIF);
Resource::Cache::get().loadSurface("whip_south.gif", "data/entities/whip_attack/whip_south.gif"); Resource::Cache::get().loadSurface("whip_south.gif", Defaults::Paths::WHIP_SOUTH_GIF);
entity_manager_.getBus().subscribe("whip_spawn", [this](const Event& ev) { entity_manager_.getBus().subscribe("whip_spawn", [this](const Event& ev) {
const auto* p = std::get_if<WhipSpawnPayload>(&ev.payload); const auto* p = std::get_if<WhipSpawnPayload>(&ev.payload);
if (p != nullptr) { spawnWhip(*p); } if (p != nullptr) { spawnWhip(*p); }
@@ -479,7 +478,7 @@ GameScene::GameScene(Config config)
// Timings i corbes de la FSM de sala. Llegits del YAML una sola // Timings i corbes de la FSM de sala. Llegits del YAML una sola
// vegada; canvis requereixen reiniciar el joc. // vegada; canvis requereixen reiniciar el joc.
transition_config_ = RoomFsm::loadConfig(Defaults::GameScene::CONFIG_YAML); transition_config_ = RoomFsm::loadConfig(Defaults::GameScene::GAME_YAML);
// Cinemàtica d'entrada de la primera sala: arrenca la pista // Cinemàtica d'entrada de la primera sala: arrenca la pista
// `room_intro.ogg` i arma el callback de fi que farà spawn + salt // `room_intro.ogg` i arma el callback de fi que farà spawn + salt
@@ -556,8 +555,7 @@ void GameScene::loadRoom(const std::string& yaml_path) {
hud_->setPyramidRoom(mgr.currentPyramid(), mgr.currentRoom()); hud_->setPyramidRoom(mgr.currentPyramid(), mgr.currentRoom());
} }
std::cout << "[ROOM] load " << yaml_path << ": entities reloaded, paint cleared, " Log::info("ROOM", "load {}: entities reloaded, paint cleared, {} active player(s) disabled (awaiting spawn cinematic)", yaml_path, ACTIVE_COUNT);
<< ACTIVE_COUNT << " active player(s) disabled (awaiting spawn cinematic)\n";
} }
void GameScene::advanceToNextRoom() { void GameScene::advanceToNextRoom() {
@@ -565,7 +563,7 @@ void GameScene::advanceToNextRoom() {
if (!mgr.advance()) { if (!mgr.advance()) {
// Política temporal mentre no hi haja pantalla de "Fi de joc": // Política temporal mentre no hi haja pantalla de "Fi de joc":
// arribar al final del levels.yaml fa loop a la primera sala. // arribar al final del levels.yaml fa loop a la primera sala.
std::cout << "[ROOM] final del levels.yaml — bucle a la primera sala\n"; Log::info("ROOM", "final del levels.yaml — bucle a la primera sala");
mgr.restart(); mgr.restart();
} }
loadRoom(mgr.currentPath()); loadRoom(mgr.currentPath());
@@ -576,9 +574,9 @@ void GameScene::enterRoomIntro() {
// Anotem la transició al log si veníem d'un altre estat; si ja // Anotem la transició al log si veníem d'un altre estat; si ja
// estàvem a INTRO (cas típic d'un salt debug) també queda ben clar. // estàvem a INTRO (cas típic d'un salt debug) també queda ben clar.
if (state_ != RoomFsm::State::ROOM_INTRO) { if (state_ != RoomFsm::State::ROOM_INTRO) {
std::cout << "[ROOM] " << RoomFsm::stateName(state_) << " -> ROOM_INTRO\n"; Log::info("ROOM", "{} -> ROOM_INTRO", RoomFsm::stateName(state_));
} else { } else {
std::cout << "[ROOM] ROOM_INTRO restart (cinemàtica reiniciada)\n"; Log::info("ROOM", "ROOM_INTRO restart (cinemàtica reiniciada)");
} }
state_ = RoomFsm::State::ROOM_INTRO; state_ = RoomFsm::State::ROOM_INTRO;
transition_ = {}; transition_ = {};
@@ -602,7 +600,7 @@ void GameScene::enterRoomIntro() {
auto* audio = Audio::get(); auto* audio = Audio::get();
if (audio == nullptr) { if (audio == nullptr) {
std::cout << "[ROOM] INTRO: Audio absent — sense música, timeline només contra text\n"; Log::warn("ROOM", "INTRO: Audio absent — sense música, timeline només contra text");
return; return;
} }
audio->stopMusic(); audio->stopMusic();
@@ -611,10 +609,9 @@ void GameScene::enterRoomIntro() {
if (Audio::getMusicState() == Audio::MusicState::PLAYING) { if (Audio::getMusicState() == Audio::MusicState::PLAYING) {
const int DUR_MS = Audio::getMusicDurationMs("room_intro.ogg"); const int DUR_MS = Audio::getMusicDurationMs("room_intro.ogg");
transition_config_.intro_music_tail_ms = std::max(0, DUR_MS - TEXT_TOTAL_MS); transition_config_.intro_music_tail_ms = std::max(0, DUR_MS - TEXT_TOTAL_MS);
std::cout << "[ROOM] INTRO: música " << DUR_MS << " ms, music_tail " Log::info("ROOM", "INTRO: música {} ms, music_tail {} ms", DUR_MS, transition_config_.intro_music_tail_ms);
<< transition_config_.intro_music_tail_ms << " ms\n";
} else { } else {
std::cout << "[ROOM] INTRO: música no arrencada — timeline només contra text\n"; Log::warn("ROOM", "INTRO: música no arrencada — timeline només contra text");
} }
} }
@@ -638,7 +635,7 @@ void GameScene::respawnPlayersInstant() {
state_ = RoomFsm::State::PLAYING; state_ = RoomFsm::State::PLAYING;
intro_completed_ = true; intro_completed_ = true;
transition_ = {}; transition_ = {};
std::cout << "[ROOM] post-editor: spawn instantani, estat -> PLAYING\n"; Log::info("ROOM", "post-editor: spawn instantani, estat -> PLAYING");
} }
#endif #endif
@@ -671,11 +668,11 @@ void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
// al YAML, l'arma queda buida i el LAND és sec igualment. // al YAML, l'arma queda buida i el LAND és sec igualment.
players_[slot_index]->armNextLandReverb(transition_config_.intro_land_reverb_preset); players_[slot_index]->armNextLandReverb(transition_config_.intro_land_reverb_preset);
std::cout << "[ROOM] spawn cinematic P" << (slot_index + 1) << ": airborne entry\n"; Log::info("ROOM", "spawn cinematic P{}: airborne entry", slot_index + 1);
} }
void GameScene::dispatchPlayerKilled(int player_id) { void GameScene::dispatchPlayerKilled(int player_id) {
std::cout << "[PLAYER KILLED] player_id=" << player_id << '\n'; Log::info("PLAYER KILLED", "player_id={}", player_id);
const auto IT = std::ranges::find_if(players_, [player_id](const auto& p) { const auto IT = std::ranges::find_if(players_, [player_id](const auto& p) {
return p && p->getId() == player_id; return p && p->getId() == player_id;
}); });
@@ -713,7 +710,7 @@ void GameScene::spawnDuePlayers() {
void GameScene::setState(RoomFsm::State new_state) { void GameScene::setState(RoomFsm::State new_state) {
if (new_state == state_) { return; } if (new_state == state_) { return; }
std::cout << "[ROOM] " << RoomFsm::stateName(state_) << " -> " << RoomFsm::stateName(new_state) << '\n'; Log::info("ROOM", "{} -> {}", RoomFsm::stateName(state_), RoomFsm::stateName(new_state));
state_ = new_state; state_ = new_state;
transition_ = {}; transition_ = {};
// Reset defensiu dels flags de cicle quan tornem a INTRO via aquest // Reset defensiu dels flags de cicle quan tornem a INTRO via aquest
@@ -742,7 +739,7 @@ void GameScene::setState(RoomFsm::State new_state) {
const int TEXT_TOTAL_MS = transition_config_.outro_text_enter_ms + transition_config_.outro_text_hold_ms + transition_config_.outro_text_exit_ms; const int TEXT_TOTAL_MS = transition_config_.outro_text_enter_ms + transition_config_.outro_text_hold_ms + transition_config_.outro_text_exit_ms;
transition_config_.outro_music_tail_ms = 0; transition_config_.outro_music_tail_ms = 0;
if (audio == nullptr) { if (audio == nullptr) {
std::cout << "[ROOM] OUTRO: Audio absent — sense música, timeline només contra text\n"; Log::warn("ROOM", "OUTRO: Audio absent — sense música, timeline només contra text");
} else { } else {
audio->stopMusic(); audio->stopMusic();
audio->setOnMusicEnded({}); audio->setOnMusicEnded({});
@@ -750,10 +747,9 @@ void GameScene::setState(RoomFsm::State new_state) {
if (Audio::getMusicState() == Audio::MusicState::PLAYING) { if (Audio::getMusicState() == Audio::MusicState::PLAYING) {
const int DUR_MS = Audio::getMusicDurationMs("room_outro.ogg"); const int DUR_MS = Audio::getMusicDurationMs("room_outro.ogg");
transition_config_.outro_music_tail_ms = std::max(0, DUR_MS - TEXT_TOTAL_MS); transition_config_.outro_music_tail_ms = std::max(0, DUR_MS - TEXT_TOTAL_MS);
std::cout << "[ROOM] OUTRO: música " << DUR_MS << " ms, music_tail " Log::info("ROOM", "OUTRO: música {} ms, music_tail {} ms", DUR_MS, transition_config_.outro_music_tail_ms);
<< transition_config_.outro_music_tail_ms << " ms\n";
} else { } else {
std::cout << "[ROOM] OUTRO: música no arrencada — timeline només contra text\n"; Log::warn("ROOM", "OUTRO: música no arrencada — timeline només contra text");
} }
} }
} }
@@ -856,7 +852,7 @@ void GameScene::renderRoomText() {
// l'antic GameCanvas::CENTER_Y per descomptar la franja del marcador // l'antic GameCanvas::CENTER_Y per descomptar la franja del marcador
// i tenir el rètol equilibrat sobre el playfield. // i tenir el rètol equilibrat sobre el playfield.
const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2; const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2;
// Colors del rètol via UiConfig (`data/ui.yaml > colors.room_text` i // Colors del rètol via UiConfig (`data/config/ui.yaml > colors.room_text` i
// `room_text_outline`). Independents dels colors de notificació. // `room_text_outline`). Independents dels colors de notificació.
const Uint8 FILL = UiConfig::get().roomText(); const Uint8 FILL = UiConfig::get().roomText();
const Uint8 OUTLINE = UiConfig::get().roomTextOutline(); const Uint8 OUTLINE = UiConfig::get().roomTextOutline();
@@ -1075,7 +1071,7 @@ void GameScene::handlePauseInput() {
pause_.curve = GOING_TO_PAUSE ? UiConfig::get().pauseEnterCurve() : UiConfig::get().pauseExitCurve(); pause_.curve = GOING_TO_PAUSE ? UiConfig::get().pauseEnterCurve() : UiConfig::get().pauseExitCurve();
pause_.state = GOING_TO_PAUSE ? PauseState::ENTERING : PauseState::EXITING; pause_.state = GOING_TO_PAUSE ? PauseState::ENTERING : PauseState::EXITING;
std::cout << "[PAUSE] " << (GOING_TO_PAUSE ? "entering" : "exiting") << " from_scale=" << pause_.from_scale << " duration_ms=" << pause_.duration_ms << '\n'; Log::info("PAUSE", "{} from_scale={} duration_ms={}", GOING_TO_PAUSE ? "entering" : "exiting", pause_.from_scale, pause_.duration_ms);
} }
} }
@@ -1212,7 +1208,7 @@ void GameScene::update(int delta_ms) {
// (creuar → player_exited → level_complete → OUTRO) es manté. // (creuar → player_exited → level_complete → OUTRO) es manté.
// Mateixos gates que el salt entre sales. // Mateixos gates que el salt entre sales.
if (pause_.state == PauseState::IDLE && !editor_.active() && detectOpenDoor()) { if (pause_.state == PauseState::IDLE && !editor_.active() && detectOpenDoor()) {
std::cout << "[DEBUG] força obertura de la porta\n"; Log::info("DEBUG", "força obertura de la porta");
entity_manager_.forceOpenDoor(); entity_manager_.forceOpenDoor();
} }
#endif #endif
+2 -2
View File
@@ -202,7 +202,7 @@ class GameScene : public Scene {
// Font del rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). // Font del rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO).
// Distinta de `pause_font_` perquè el rètol de fase té estètica // Distinta de `pause_font_` perquè el rètol de fase té estètica
// pròpia (CaesarDressing per defecte, configurable via // pròpia (CaesarDressing per defecte, configurable via
// `data/ui.yaml > fonts.room_text`). La pausa segueix amb la // `data/config/ui.yaml > fonts.room_text`). La pausa segueix amb la
// placeholder. // placeholder.
std::shared_ptr<Text> room_text_font_; std::shared_ptr<Text> room_text_font_;
@@ -224,7 +224,7 @@ class GameScene : public Scene {
} pause_; } pause_;
// Configuració de la FSM de sala (intro/outro). Carregada al ctor des // Configuració de la FSM de sala (intro/outro). Carregada al ctor des
// de `data/scenes/game.yaml`; valors per defecte si manca el YAML. // de `data/scenes/game/game.yaml`; valors per defecte si manca el YAML.
RoomFsm::Config transition_config_{}; RoomFsm::Config transition_config_{};
// Estat actual de la FSM de sala. S'inicialitza a ROOM_INTRO al ctor // Estat actual de la FSM de sala. S'inicialitza a ROOM_INTRO al ctor
+2 -4
View File
@@ -4,7 +4,6 @@
#include <algorithm> #include <algorithm>
#include <cstdint> #include <cstdint>
#include <iostream>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -20,13 +19,12 @@
#include "game/scenes/scene_flow.hpp" #include "game/scenes/scene_flow.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
#include "utils/log.hpp"
namespace { namespace {
constexpr int NAME_Y_OFFSET = -8; constexpr int NAME_Y_OFFSET = -8;
constexpr int TIMER_Y_OFFSET = 8; constexpr int TIMER_Y_OFFSET = 8;
// TODO: migrar a un logger unificat quan es decideixi (veure INC-03 a
// docs/audit-core-rendering.md).
const char* const SCENE_NAME = "INTRO"; const char* const SCENE_NAME = "INTRO";
} // namespace } // namespace
@@ -34,7 +32,7 @@ namespace {
IntroScene::IntroScene(std::shared_ptr<Text> font, DebugConfig::StartingPlayers starting_players) IntroScene::IntroScene(std::shared_ptr<Text> font, DebugConfig::StartingPlayers starting_players)
: font_(std::move(font)), : font_(std::move(font)),
starting_players_(starting_players) { starting_players_(starting_players) {
std::cout << "[scene] " << SCENE_NAME << " start (players=" << DebugConfig::toString(starting_players_) << ")\n"; Log::info("scene", "{} start (players={})", SCENE_NAME, DebugConfig::toString(starting_players_));
} }
IntroScene::~IntroScene() = default; IntroScene::~IntroScene() = default;
+138 -32
View File
@@ -4,41 +4,111 @@
#include <algorithm> #include <algorithm>
#include <cstdint> #include <cstdint>
#include <iostream>
#include <string>
#include <utility> #include <utility>
#include "core/audio/audio.hpp"
#include "core/input/global_inputs.hpp" #include "core/input/global_inputs.hpp"
#include "core/rendering/notifier.hpp" #include "core/rendering/notifier.hpp"
#include "core/rendering/render_info.hpp" #include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/surface.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp" #include "core/ui/ui_config.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/scenes/placeholder_input.hpp" #include "game/scenes/placeholder_input.hpp"
#include "game/scenes/scene_flow.hpp" #include "game/scenes/scene_flow.hpp"
#include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
#include "utils/log.hpp"
namespace { namespace {
constexpr int NAME_Y_OFFSET = -8;
constexpr int TIMER_Y_OFFSET = 8;
// TODO: migrar a un logger unificat quan es decideixi (veure INC-03 a
// docs/audit-core-rendering.md). De moment std::cout per rastrejar el
// flux d'escenes als smoke runs.
const char* const SCENE_NAME = "LOGO"; const char* const SCENE_NAME = "LOGO";
} // namespace } // namespace
LogoScene::LogoScene(std::shared_ptr<Text> font) LogoScene::LogoScene(std::shared_ptr<Surface> logo, bool reset)
: font_(std::move(font)) { : logo_(std::move(logo)) {
std::cout << "[scene] " << SCENE_NAME << " start\n"; Log::info("scene", "{} start{}", SCENE_NAME, reset ? " (reset)" : "");
if (reset) {
if (auto* audio = Audio::get(); audio != nullptr) {
audio->stopMusic();
audio->stopAllSounds();
}
}
// Arrenquem en BLACK_INTRO: ni so ni efecte. El glitch sound i el
// begin() de scan_ es disparen quan entrem a SCAN.
} }
LogoScene::~LogoScene() = default; LogoScene::~LogoScene() = default;
auto LogoScene::wantsSkip() -> bool {
return std::ranges::any_of(EventBuffer::events, [](const SDL_Event& ev) {
return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode);
});
}
void LogoScene::enterScan() {
phase_ = Phase::SCAN;
phase_elapsed_ms_ = 0;
// So del scanline reveal: una sola pulsació al començament de la
// fase SCAN, en grup INTERFACE perquè no es vegi afectat pel volum
// de sons del joc i quedi sempre audible.
if (auto* audio = Audio::get(); audio != nullptr) {
audio->playSound(UiConfig::get().logoGlitchSound(), Audio::Group::INTERFACE);
}
// Arranca el revelat CRT: el primer frame ja pinta zero files (banda
// damunt del logo) i avança fins a tenir-lo visible al final.
const auto& s = UiConfig::get().logoScan();
scan_.begin({
.duration_ms = s.duration_ms,
.band_height_px = s.band_height_px,
.band_alpha = s.band_alpha,
.band_pulse_hz = s.band_pulse_hz,
.band_pulse_depth = s.band_pulse_depth,
.glitch_amplitude_px = s.glitch_amplitude_px,
.glitch_band_radius_px = s.glitch_band_radius_px,
.vhs_burst_per_sec = s.vhs_burst_per_sec,
.vhs_burst_duration_ms = s.vhs_burst_duration_ms,
.vhs_burst_height_px = s.vhs_burst_height_px,
.vhs_burst_amplitude_px = s.vhs_burst_amplitude_px,
.flash_ms = s.flash_ms,
.tail_glitch_ms = s.tail_glitch_ms,
});
}
void LogoScene::enterHoldSilent() {
phase_ = Phase::HOLD_SILENT;
phase_elapsed_ms_ = 0;
}
void LogoScene::enterHoldMusic() {
phase_ = Phase::HOLD_MUSIC;
phase_elapsed_ms_ = 0;
// Música del títol al començament d'aquesta fase. La crida és
// idempotent (Audio detecta same-track + same-loop i no reinicia),
// així que TitleScene podrà fer el seu propi playMusic sense
// interrompre la pista durant el FADE_OUT i el BLACK_OUTRO.
if (auto* audio = Audio::get(); audio != nullptr) {
audio->playMusic("title.ogg", -1);
}
}
void LogoScene::enterFadeOut() {
phase_ = Phase::FADE_OUT;
phase_elapsed_ms_ = 0;
fade_.begin(UiConfig::get().logoTimeline().fade_out_ms, Transitions::FadeToBlack::Direction::FADE_OUT);
}
void LogoScene::enterBlackOutro() {
phase_ = Phase::BLACK_OUTRO;
phase_elapsed_ms_ = 0;
}
void LogoScene::enterDone() {
phase_ = Phase::DONE;
exit_request_ = ExitRequest{.next = SceneId::TITLE, .params = TitleParams{}};
}
void LogoScene::update(int delta_ms) { void LogoScene::update(int delta_ms) {
GlobalInputs::update(delta_ms); GlobalInputs::update(delta_ms);
if (GlobalInputs::wantsQuit()) { if (GlobalInputs::wantsQuit()) {
@@ -46,14 +116,44 @@ void LogoScene::update(int delta_ms) {
return; return;
} }
timer_ms_ -= delta_ms; // Skip: qualsevol tecla no reservada salta directament a la
// transició cap a TITLE. El propi TitleScene fa el seu fade-in
// inicial des de negre, així que la sortida no queda abrupta.
if (phase_ != Phase::DONE && wantsSkip()) {
enterDone();
}
const bool SKIPPED = std::ranges::any_of(EventBuffer::events, [](const SDL_Event& ev) { if (phase_ != Phase::DONE) {
return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode); phase_elapsed_ms_ += delta_ms;
}); // Tick dels efectes actius. Tots dos són no-op si el seu
// duration_ms intern està a 0; només cal cridar-los si la fase
// corresponent està viva o just acabada.
scan_.tick(delta_ms);
fade_.tick(delta_ms);
}
if (timer_ms_ <= 0 || SKIPPED) { const auto& tl = UiConfig::get().logoTimeline();
exit_request_ = ExitRequest{.next = SceneId::TITLE, .params = TitleParams{}}; switch (phase_) {
case Phase::BLACK_INTRO:
if (phase_elapsed_ms_ >= tl.black_intro_ms) { enterScan(); }
break;
case Phase::SCAN:
if (scan_.isFinished()) { enterHoldSilent(); }
break;
case Phase::HOLD_SILENT:
if (phase_elapsed_ms_ >= tl.hold_silent_ms) { enterHoldMusic(); }
break;
case Phase::HOLD_MUSIC:
if (phase_elapsed_ms_ >= tl.hold_with_music_ms) { enterFadeOut(); }
break;
case Phase::FADE_OUT:
if (phase_elapsed_ms_ >= tl.fade_out_ms) { enterBlackOutro(); }
break;
case Phase::BLACK_OUTRO:
if (phase_elapsed_ms_ >= tl.black_outro_ms) { enterDone(); }
break;
case Phase::DONE:
break;
} }
Notifier::get().update(delta_ms); Notifier::get().update(delta_ms);
@@ -64,23 +164,29 @@ void LogoScene::render() {
constexpr std::uint32_t BLACK_ARGB = 0xFF000000U; constexpr std::uint32_t BLACK_ARGB = 0xFF000000U;
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BLACK_ARGB); Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BLACK_ARGB);
if (font_) { // BLACK_INTRO i BLACK_OUTRO no pinten logo: només el negre del
const int SECS_LEFT = std::max(1, (timer_ms_ + 999) / 1000); // fillRect d'aquí dalt. La resta de fases sí.
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, const bool SHOW_LOGO = phase_ != Phase::BLACK_INTRO && phase_ != Phase::BLACK_OUTRO;
GameCanvas::CENTER_X, if (logo_ && SHOW_LOGO) {
GameCanvas::CENTER_Y + NAME_Y_OFFSET, const int X = (GameCanvas::WIDTH - logo_->getWidth()) / 2;
SCENE_NAME, const int Y = (GameCanvas::HEIGHT - logo_->getHeight()) / 2;
1, if (phase_ == Phase::SCAN) {
UiConfig::get().notificationText()); // Revelat CRT + glitch tail: la pròpia ScanReveal s'encarrega
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, // del blit fila a fila, de la banda lluminosa, dels glitches,
GameCanvas::CENTER_X, // del flash final i dels bursts del tail.
GameCanvas::CENTER_Y + TIMER_Y_OFFSET, scan_.render(*logo_, X, Y);
std::to_string(SECS_LEFT), } else {
1, // HOLD_SILENT, HOLD_MUSIC, FADE_OUT: render normal, el logo
UiConfig::get().notificationText()); // ja està a pantalla complet i estable.
logo_->render(X, Y);
}
} }
Notifier::get().render(); Notifier::get().render();
// Capa de fosa: només pinta durant FADE_OUT (alpha creixent). En
// BLACK_OUTRO ja deixem la pantalla negra del fillRect, així que no
// cal ni que `fade_` segueixi viu.
fade_.render();
RenderInfo::get().render(); RenderInfo::get().render();
Screen::get().present(); Screen::get().present();
} }
+50 -10
View File
@@ -1,26 +1,66 @@
#pragma once #pragma once
#include <cstdint>
#include <memory> #include <memory>
#include "core/rendering/effects/scan_reveal.hpp"
#include "core/rendering/transitions/fade_to_black.hpp"
#include "core/scene/scene.hpp" #include "core/scene/scene.hpp"
class Text; class Surface;
// Escena placeholder del logotip. Durada fixa (5 s) o skip amb qualsevol tecla // Escena del logotip de l'editora. Timeline lineal en 7 fases visibles
// no reservada. Quan expira, publica un `ExitRequest` cap a TITLE; Director // (durades configurables des de `logo.timeline.*` al YAML, excepte SCAN
// és qui construeix la nova escena (l'escena no coneix els altres tipus i // que la marca `logo.scan.duration_ms` + `tail_glitch_ms` + `flash_ms`):
// es pot exercir en tests sense arrossegar dependències). //
// `BLACK_INTRO` — negre pur abans d'arrencar res.
// `SCAN` — revelat scanline CRT amb glitch tail. So
// `logoGlitchSound` es dispara just al començar
// aquesta fase (no al constructor).
// `HOLD_SILENT` — logo estable, en silenci, marge entre el final
// del glitch i l'arrencada de la música.
// `HOLD_MUSIC` — logo estable amb la música del títol sonant.
// `FADE_OUT` — fade lineal a negre.
// `BLACK_OUTRO` — negre amb la música encara sonant. TitleScene
// continuarà la pista de manera idempotent (Audio
// detecta same-track + same-loop i no reinicia).
// `DONE` — terminal; dispara la transició cap a TITLE.
//
// Skip: qualsevol tecla no reservada salta directament a la transició
// (sense fade-out) — el TitleScene té el seu propi `intro.fade_in_ms`,
// així que la transició queda visualment continua igualment.
//
// Si arriba amb `reset=true` (acció "Reiniciar" del menú de servei),
// atura música i tots els sons al constructor — el següent LOGO arrenca
// silenciós encara que veníssem del joc en marxa.
class LogoScene : public Scene { class LogoScene : public Scene {
public: public:
static constexpr int DURATION_MS = 5000; LogoScene(std::shared_ptr<Surface> logo, bool reset);
explicit LogoScene(std::shared_ptr<Text> font);
~LogoScene() override; ~LogoScene() override;
void update(int delta_ms) override; void update(int delta_ms) override;
void render() override; void render() override;
private: private:
std::shared_ptr<Text> font_; enum class Phase : std::uint8_t { BLACK_INTRO,
int timer_ms_{DURATION_MS}; SCAN,
HOLD_SILENT,
HOLD_MUSIC,
FADE_OUT,
BLACK_OUTRO,
DONE };
void enterScan();
void enterHoldSilent();
void enterHoldMusic();
void enterFadeOut();
void enterBlackOutro();
void enterDone();
[[nodiscard]] static auto wantsSkip() -> bool;
std::shared_ptr<Surface> logo_;
Phase phase_{Phase::BLACK_INTRO};
int phase_elapsed_ms_{0};
Transitions::FadeToBlack fade_;
Effects::ScanReveal scan_;
}; };
+1 -1
View File
@@ -8,7 +8,7 @@
namespace Placeholder { namespace Placeholder {
// Retorna true si el scancode coincideix amb alguna tecla declarada al // Retorna true si el scancode coincideix amb alguna tecla declarada al
// KeyConfig::get() (les carregades de data/input/keys.yaml). Si KeyConfig // KeyConfig::get() (les carregades de data/config/input.yaml). Si KeyConfig
// no s'ha carregat, totes les consultes retornaran SDL_SCANCODE_UNKNOWN i // no s'ha carregat, totes les consultes retornaran SDL_SCANCODE_UNKNOWN i
// aquesta funció tornarà false, de manera que el test/smoke no depèn de // aquesta funció tornarà false, de manera que el test/smoke no depèn de
// l'estat global de la configuració. // l'estat global de la configuració.
+3 -5
View File
@@ -1,12 +1,12 @@
#include "game/scenes/room_fsm.hpp" #include "game/scenes/room_fsm.hpp"
#include <array> #include <array>
#include <iostream>
#include <string_view> #include <string_view>
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace { namespace {
@@ -28,7 +28,7 @@ namespace RoomFsm {
auto bytes = Resource::Helper::loadFile(yaml_path); auto bytes = Resource::Helper::loadFile(yaml_path);
if (bytes.empty()) { if (bytes.empty()) {
std::cout << "RoomFsm: " << yaml_path << " absent — uso fallbacks compilats\n"; Log::warn("RoomFsm", "{} absent — uso fallbacks compilats", yaml_path);
return cfg; return cfg;
} }
const auto* begin = reinterpret_cast<const char*>(bytes.data()); const auto* begin = reinterpret_cast<const char*>(bytes.data());
@@ -83,9 +83,7 @@ namespace RoomFsm {
cfg.text_panel_height_px = readChildInt(n, "height_px", cfg.text_panel_height_px); cfg.text_panel_height_px = readChildInt(n, "height_px", cfg.text_panel_height_px);
} }
std::cout << "RoomFsm: timings carregats des de " << yaml_path Log::info("RoomFsm", "timings carregats des de {} (intro fade {}ms, outro fade {}ms)", yaml_path, cfg.intro_fade_in_ms, cfg.outro_fade_out_ms);
<< " (intro fade " << cfg.intro_fade_in_ms
<< "ms, outro fade " << cfg.outro_fade_out_ms << "ms)\n";
return cfg; return cfg;
} }
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff. Show more