Compare commits

...
35 Commits
Author SHA1 Message Date
JailDesigner 2cbf1bb6e0 chore(version): bump 0.39.0 -> 0.40.0 2026-05-08 23:00:06 +02:00
JailDesigner 8890fa9e1f Merge branch 'feat/player-join-and-game-over' 2026-05-08 22:59:47 +02:00
JailDesigner 23f7cb03a6 chore(data): restaura lives:3 a sam i morcus despres de proves 2026-05-08 22:59:43 +02:00
JailDesigner 486a440668 feat(game): skip de countdown amb JUMP/ATTACK quan numero <= 7 2026-05-08 22:20:39 +02:00
JailDesigner a717411edd feat(audio): musica acelerada en CONTINUE?, game_over.ogg, tick del countdown 2026-05-08 22:06:44 +02:00
JailDesigner 5e862ceadc fix(game): HUD lives desde YAML, prompt P1 en single-P2, countdown 9..0 configurable 2026-05-08 21:14:33 +02:00
JailDesignerandClaude Opus 4.7 78e164fce4 feat(game): CONTINUE? + GAME OVER + fade-to-LOGO (Fase 2)
Sub-FSM nova `GameOverFsm` (CONTINUE_PROMPT → GAME_OVER → FADE_OUT →
DONE) que viu en paral·lel a la `RoomFsm`. Es dispara quan tots els
jugadors queden DISABLED durant PLAYING, mostra el rètol "Continuar?"
amb countdown reusant el panel del room text, i si ningú pulsa START
acaba en "Fi del Joc" + fade negre i sol·licita la transició a LOGO.
Pulsar START durant CONTINUE? cancel·la el countdown i fa join (revive)
del jugador. Després de GAME OVER, el HUD passa a "ESPERA / PER JUGAR"
i el START queda bloquejat fins a la transició d'escena.

Els helpers d'animació del room text (X i altura del panel) ara reben
phase + elapsed_ms per paràmetre per poder ser reaprofitats per les
dues sub-FSMs sense duplicar la lògica visual.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:30:37 +02:00
JailDesignerandClaude Opus 4.7 c13b11c501 feat(player): join in progress + DISABLED en agotar vidas (Fase 1)
Habilita la regla 1 de la nova FSM de joc: durant PLAYING, qualsevol
slot DISABLED pot pulsar START per entrar a partida amb spawn aeri al
seu costat de la porta i vides reseteades. Quan un jugador esgota les
vides, `respawnFromDying` el deixa a DISABLED i publica `player_disabled`
al bus; GameScene i HUD reaccionen, deixant el slot disponible per a un
nou join. Afegeix prompts al HUD ("PULSA START / PER JUGAR" o
"ESPERA / PER JUGAR") segons l'estat de la RoomFsm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:04:33 +02:00
JailDesigner 8921b9fd0c chore(version): bump 0.38.0 -> 0.39.0 2026-05-08 13:43:44 +02:00
JailDesigner 1d9fea28fe Merge branch 'feat/gameplay-tweaks'
Tweaks de jugabilitat persistents (gameplay.yaml a la carpeta de
sistema) amb tres flags i submenú "Jugabilitat" al ServiceMenu:
látec travessa sarcs, foc amic, col·lisió entre jugadors.
2026-05-08 13:43:24 +02:00
JailDesigner d0d5cb0e66 fix(player): players_collide també anul·la resolver i landing overlap 2026-05-08 13:40:30 +02:00
JailDesigner e25ccd02fd feat(menu): submenú "Jugabilitat" al menú de servei amb tres toggles
Nou apartat al ServiceMenu (entre Controls i Sistema) amb tres toggles
ON/OFF connectats a GameplayConfig: látec travessa sarcs, foc amic,
col·lisió entre jugadors. Cada canvi crida save() immediatament, així
l'estat persisteix entre sessions sense necessitat de "aplicar".

Locales ca/en: claus `tweaks` + tres `menu.items.*` corresponents.
2026-05-08 13:29:04 +02:00
JailDesigner 3635cebf25 feat(player): cabla flag players.collide_with_each_other
A `collidesWithWorld` saltem la branca actor↔actor quan el flag està
desactivat: els jugadors travessen els altres lliurement però segueixen
topant amb murs i sarcs. A `tryPushSnapBlockers` retornem early per la
mateixa raó: si no hi ha col·lisió entre jugadors, no hi ha ningú a
empentar quan el snap del corredor s'aplica.
2026-05-08 13:26:47 +02:00
JailDesigner cfbcdc236b feat(whip): cabla flag combat.friendly_fire a hitPlayerIfApplicable
Quan el flag està desactivat, el látigo no fa dany a altres jugadors
(l'owner ja quedava exclòs per la guarda existent). Default true:
manté el comportament actual.

Sense canvis als tests del whip — friendly fire ja no es cobreix
allà perquè els stubs de Player no porten estat real (vegeu el comentari
del top de test_whip_attack.cpp).
2026-05-08 13:24:33 +02:00
JailDesigner 7871d3930e feat(whip): cabla flag whip.damage_pierces_walls a computeDamageRect
Quan el flag està actiu (default), l'iteració píxel a píxel ignora
`Map::collides` i només respecta `insidePlayfield`. El mur extern,
doncs, sempre retalla — fora del playfield no hi ha res a colpejar.

Els tests existents passen amb la nova lògica perquè els seus setups
posen el damage_rect a corredors lliures (sense sarcs a la trajectòria),
i el test W2 segueix verificant el retall del playfield.
2026-05-08 13:22:52 +02:00
JailDesigner 932b457725 feat(config): afegeix GameplayConfig persistent amb versionatge defensiu
Singleton paral·lel a UiConfig però escrivible: viu a la carpeta de
sistema (gameplay.yaml). Defaults compilats: friendly_fire,
players_collide i whip_damage_pierces_walls a true. Davant qualsevol
irregularitat (fitxer absent/buit, parse error, version absent o
desconeguda, clau requerida mal tipada o absent), reseteja a defaults
i reescriu el fitxer en lloc de preservar estat parcial.

Cablejat al Director::start després de Options::loadFromFile. Encara
sense consumidors — els flags es cablejaran a WhipAttack i al sistema
de col·lisió de jugadors en commits posteriors.
2026-05-08 13:17:28 +02:00
JailDesigner e9f99a0542 fix(mummy): evita soft-lock per drift sub-pixel a HURT vora del mur 2026-05-08 12:07:18 +02:00
JailDesigner 0d58da536c chore(version): bump 0.37.0 -> 0.38.0 2026-05-08 11:16:22 +02:00
JailDesigner 583acb9053 fix(config): rutes per plataforma a createSystemFolder 2026-05-08 11:15:15 +02:00
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
130 changed files with 2846 additions and 831 deletions

No files matched your search

+2 -2
View File
@@ -8,8 +8,8 @@ dist/
*.dylib
*.dll
*.exe
aee_2026
_aee_2026
aee_arcade
_aee_arcade
.vscode/
.idea/
.cache/
+14 -1
View File
@@ -1,5 +1,5 @@
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)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
@@ -104,18 +104,22 @@ set(APP_SOURCES
source/core/resources/resource_pack.cpp
source/core/resources/palette_loader.cpp
source/utils/easing.cpp
source/utils/log.cpp
source/utils/utils.cpp
source/game/debug_config.cpp
source/game/gameplay_config.cpp
source/game/options.cpp
source/game/options_apply.cpp
source/game/ui/service_menu.cpp
source/game/ui/hud.cpp
source/game/scenes/placeholder_input.cpp
source/game/scenes/start_input.cpp
source/game/scenes/logo_scene.cpp
source/game/scenes/title_scene.cpp
source/game/scenes/intro_scene.cpp
source/game/scenes/game_scene.cpp
source/game/scenes/room_fsm.cpp
source/game/scenes/game_over_fsm.cpp
source/game/entities/player.cpp
source/game/map/map.cpp
source/game/map/tilemap.cpp
@@ -218,8 +222,10 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp
source/utils/log.cpp
)
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)
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
@@ -361,9 +367,11 @@ if(BUILD_TESTS)
add_executable(test_entities_smoke
tests/test_entities_smoke.cpp
tests/hole_player_stubs.cpp
tests/audio_cache_stubs.cpp
source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp
source/game/map/map.cpp
source/utils/log.cpp
${ENTITY_SOURCES_BASE}
)
target_include_directories(test_entities_smoke PRIVATE
@@ -389,9 +397,12 @@ if(BUILD_TESTS)
tests/test_whip_attack.cpp
tests/whip_attack_player_stubs.cpp
tests/hole_player_stubs.cpp
tests/audio_cache_stubs.cpp
source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp
source/game/gameplay_config.cpp
source/game/map/map.cpp
source/utils/log.cpp
${ENTITY_SOURCES_BASE}
source/game/entities/concrete/whip_attack.cpp
)
@@ -416,6 +427,7 @@ if(BUILD_TESTS)
source/core/input/key_config.cpp
source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp
source/utils/log.cpp
)
target_include_directories(test_scene_flow PRIVATE
"${CMAKE_SOURCE_DIR}/source"
@@ -455,6 +467,7 @@ if(BUILD_TESTS)
source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp
source/utils/easing.cpp
source/utils/log.cpp
)
target_include_directories(test_game_fsm PRIVATE
"${CMAKE_SOURCE_DIR}/source"
+1 -1
View File
@@ -1,4 +1,4 @@
PROJECT := aee_2026
PROJECT := aee_arcade
BUILDDIR := build
# Detecció de plataforma. En Windows CMake defaulteja a "NMake Makefiles"
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.
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
# de moviment del jugador viuen a part (Defaults::KeysGame); ací només hi ha
File renamed without changes.
+115
View File
@@ -0,0 +1,115 @@
# Tema de la UI: índexs de paleta, timings, fonts i flags visuals. Tota
# clau absent cau al valor compilat a Defaults::UI (veure defaults.hpp).
colors:
render_info_text: 58
render_info_shadow: 16
canvas_bg: 72
notification_text: 232
notification_bg: 64
exit_text: 71
exit_bg: 2
menu_bg: 2
menu_border: 232
menu_title: 237
menu_label: 58
menu_value: 232
menu_cursor: 71
menu_disabled: 16
hud_bg: 9
hud_label: 58
hud_value: 187
hud_flash: 237
# Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). Independents
# de notification_* perquè la fase i les notificacions són dos canals
# visuals distints: ací pots canviar el color del rètol sense tocar
# els missatges del HUD.
room_text: 160
room_text_outline: 253
timings:
notification_ms: 4000
exit_ms: 4000
fonts:
hud:
name: "8bithud"
gif: "data/fonts/8bithud.gif"
fnt: "data/fonts/8bithud.fnt"
kerning: 1
placeholder:
name: "ari-w9500-condensed-display"
gif: "data/fonts/ari-w9500-condensed-display.gif"
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
kerning: 1
# Font del rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO).
room_text:
name: "CaesarDressing-Regular"
gif: "data/fonts/CaesarDressing-Regular.gif"
fnt: "data/fonts/CaesarDressing-Regular.fnt"
kerning: 1
menu_title:
name: "ari-w9500-display"
gif: "data/fonts/ari-w9500-display.gif"
fnt: "data/fonts/ari-w9500-display.fnt"
kerning: 0
menu_items:
name: "ari-w9500-condensed-display"
gif: "data/fonts/ari-w9500-condensed-display.gif"
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
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:
bg_enabled: false
entry_ms: 800 # durada de l'animació d'entrada dels tres blocs
flash_ms: 250 # durada del flash quan un jugador rep un hit
shake_ms: 300 # durada del shake que acompanya el flash
# Geometria del popup superior (Notifier). En píxels del canvas, llevat de
# slide_ms (durada en ms de l'animació d'entrada i d'eixida; constant
# independentment de la mida de la caixa). El padding vertical és asimètric a
# propòsit: padding_top > padding_bottom puja una mica el text dins la caixa.
# Corbes disponibles: linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
notifier:
padding_h: 6
padding_top: 4
padding_bottom: 3
line_gap: 1
margin_top: 3
border_px: 1
slide_ms: 300
slide_in_curve: outCubic
slide_out_curve: inCubic
# Pausa graduada de la GameScene: el món no para d'un colp, s'atura amb
# easing i torna a arrancar amb una altra corba. Els timings es poden
# afinar independentment, i les corbes permeten estils diferents entrant
# i sortint (p.ex. frenar suau i arrancar seca). Corbes disponibles:
# linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
pause:
enter_ms: 400
exit_ms: 250
enter_curve: outCubic
exit_curve: inCubic
# Sons del menú de servei. Els fitxers viuen a data/audio/sounds/. SELECT
# acompanya navegació (amunt/avall) i back; ACCEPT acompanya modificació
# d'opció (esquerra/dreta/retorn) o entrada a un submenú.
sounds:
menu_select: "menu_select.wav"
menu_accept: "menu_accept.wav"
# Disparat quan un jugador prem START a la TITLE (primera pulsació o
# la del segon jugador dins de la finestra de STARTING).
player_start: "player_start.wav"
# Soroll/scratch sincronitzat amb la fase SCAN de LogoScene (scanline
# reveal). Es dispara al constructor de l'escena.
logo_glitch: "glitch.wav"
+3
View File
@@ -58,3 +58,6 @@ hurt:
duration_ms: 600
speed: 60
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_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 }
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 }
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 }
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true }
attack_south: { frames: [20, 21, 22, 23], frame_ms: 80 }
attack_north: { frames: [24, 25, 26, 27], frame_ms: 80 }
attack_east: { frames: [28, 29, 30, 31], frame_ms: 80 }
attack_west: { frames: [28, 29, 30, 31], frame_ms: 80, flip_h: true }
recover_south: { frames: [23] }
recover_north: { frames: [27] }
@@ -119,7 +119,7 @@ hurt:
# recuperació en què la hitbox ja no fa dany.
whip:
range_tiles: 3
swing_ms: 200
swing_ms: 320
cooldown_ms: 200
damage: 1
sound: whip.wav
@@ -136,7 +136,7 @@ death:
# poden ferir ni matar el jugador (els forats sí que el maten — la
# caiguda no és dany convencional). Visualment es manifesta amb un
# 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
# mecànica queda desactivada i el comportament és l'antic.
immunity:
File renamed without changes.
@@ -40,10 +40,10 @@ animations:
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
attack_south: { frames: [20, 21, 22, 23], frame_ms: 50 }
attack_north: { frames: [24, 25, 26, 27], frame_ms: 50 }
attack_east: { frames: [28, 29, 30, 31], frame_ms: 50 }
attack_west: { frames: [28, 29, 30, 31], frame_ms: 50, flip_h: true }
attack_south: { frames: [20, 21, 22, 23], frame_ms: 80 }
attack_north: { frames: [24, 25, 26, 27], frame_ms: 80 }
attack_east: { frames: [28, 29, 30, 31], frame_ms: 80 }
attack_west: { frames: [28, 29, 30, 31], frame_ms: 80, flip_h: true }
recover_south: { frames: [23] }
recover_north: { frames: [27] }
@@ -119,7 +119,7 @@ hurt:
# recuperació en què la hitbox ja no fa dany.
whip:
range_tiles: 3
swing_ms: 200
swing_ms: 320
cooldown_ms: 200
damage: 1
sound: whip.wav
@@ -136,7 +136,7 @@ death:
# poden ferir ni matar el jugador (els forats sí que el maten — la
# caiguda no és dany convencional). Visualment es manifesta amb un
# 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
# mecànica queda desactivada i el comportament és l'antic.
immunity:
+2 -2
View File
@@ -25,7 +25,7 @@ sprites:
# 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.
# - `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`).
# - `active_frames` : valors dins de `frames` durant els quals la
# hitbox aplica dany. Wind-up (0) i recovery (3) no
@@ -33,6 +33,6 @@ sprites:
animations:
swing:
frames: [0, 1, 2, 3]
frame_ms: 50
frame_ms: 80
loop: false
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

+19 -1
View File
@@ -1,4 +1,4 @@
# AEE 2026 — Català
# AEE ARCADE — Català
# lang: ca
notification:
@@ -38,6 +38,13 @@ hud:
treasures: "Tres"
pyramid: "Piramide"
room: "Sala"
# Prompts mostrats al bloc d'un slot inactiu (en lloc dels zeros)
# quan un jugador encara no ha pulsat START o ha esgotat vides.
# Dues línies per fer-los caber al bloc lateral (~106 px).
join_prompt_can_join_l1: "POLSA START"
join_prompt_can_join_l2: "PER JUGAR"
join_prompt_wait_l1: "ESPERA"
join_prompt_wait_l2: "PER JUGAR"
# Textos de l'escena TITLE.
title:
@@ -53,6 +60,12 @@ game:
room_intro_room: "Sala {r}"
room_complete_line1: "Fase"
room_complete_line2: "Completada"
# Rètol del game over (sub-FSM de la GameScene). El countdown es
# pinta com a segona línia (un enter) durant la fase 1 de
# CONTINUE_PROMPT — només cal traduir el rètol superior.
continue_prompt: "Continuar?"
game_over_line1: "Fi del"
game_over_line2: "Joc"
menu:
titles:
@@ -62,6 +75,7 @@ menu:
controls: "Controls"
keyboard: "Teclat"
gamepad: "Comandament"
tweaks: "Jugabilitat"
system: "Sistema"
labels:
player_1: "Jugador 1"
@@ -73,6 +87,7 @@ menu:
controls_submenu: "Controls"
keyboard_submenu: "Teclat"
gamepad_submenu: "Comandament"
tweaks_submenu: "Jugabilitat"
system_submenu: "Sistema"
move_up: "Amunt"
move_down: "Avall"
@@ -101,6 +116,9 @@ menu:
music_volume: "Volum música"
sounds: "Sons"
sounds_volume: "Volum sons"
whip_pierces_walls: "Látec travessa sarcs"
friendly_fire: "Foc amic"
players_collide: "Col·lisió entre jugadors"
restart: "Reiniciar"
exit_game: "Eixir del joc"
values:
+19 -1
View File
@@ -1,4 +1,4 @@
# AEE 2026 — English
# AEE ARCADE — English
# lang: en
notification:
@@ -38,6 +38,13 @@ hud:
treasures: "Tre"
pyramid: "Pyramid"
room: "Room"
# Prompts shown in an inactive slot's block (instead of zeros) while
# a player hasn't pressed START yet or has run out of lives. Two
# short lines so they fit in the side block (~106 px wide).
join_prompt_can_join_l1: "PRESS START"
join_prompt_can_join_l2: "TO PLAY"
join_prompt_wait_l1: "PLEASE"
join_prompt_wait_l2: "WAIT"
# Texts of the TITLE scene.
title:
@@ -52,6 +59,12 @@ game:
room_intro_room: "Room {r}"
room_complete_line1: "Stage"
room_complete_line2: "Complete"
# Game over labels (sub-FSM in GameScene). The countdown is drawn as
# a numeric second line during CONTINUE_PROMPT phase 1, so only the
# top label needs translation.
continue_prompt: "Continue?"
game_over_line1: "Game"
game_over_line2: "Over"
menu:
titles:
@@ -61,6 +74,7 @@ menu:
controls: "Controls"
keyboard: "Keyboard"
gamepad: "Gamepad"
tweaks: "Gameplay"
system: "System"
labels:
player_1: "Player 1"
@@ -72,6 +86,7 @@ menu:
controls_submenu: "Controls"
keyboard_submenu: "Keyboard"
gamepad_submenu: "Gamepad"
tweaks_submenu: "Gameplay"
system_submenu: "System"
move_up: "Up"
move_down: "Down"
@@ -100,6 +115,9 @@ menu:
music_volume: "Music volume"
sounds: "Sounds"
sounds_volume: "Sound volume"
whip_pierces_walls: "Whip pierces sarcs"
friendly_fire: "Friendly fire"
players_collide: "Player collision"
restart: "Restart"
exit_game: "Exit game"
values:
-48
View File
@@ -1,48 +0,0 @@
# 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
# i font del text es reutilitzen del tema UI (data/ui.yaml > pause +
# notification). Si manca el fitxer o alguna clau, GameScene cau als
# fallbacks compilats; veure `readSceneConfig` a game_scene.cpp.
room_intro:
fade_in_ms: 500 # negre opac → món visible
text_delay_ms: 500 # espera amb el món net abans de sortir el text
text_enter_ms: 400 # text esquerra → centre
text_hold_ms: 2000 # text quiet al centre
text_exit_ms: 400 # text centre → fora per l'esquerra
line_gap_px: -4 # px extra entre les dues línies (piràmide / sala)
# Delays d'spawn aeri **mesurats des de la fi de la música d'INTRO**
# (callback `Audio::setOnMusicEnded`). 0 = cau al moment exacte que
# `room_intro.ogg` acaba; valors > 0 escalonen els jugadors. En mode
# single-player s'usa només `player_spawn_delay_ms`.
player_spawn_delay_ms: 0
player2_spawn_delay_ms: 300
# Preset de reverb (catàleg a `data/sounds.yaml`) aplicat al primer
# 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
# post-mort cauen al so sec. Buit o absent = sense reverb.
land_reverb: stone_hall
room_outro:
text_enter_ms: 400
text_hold_ms: 2000
text_exit_ms: 400
fade_out_ms: 500 # món visible → negre opac
line_gap_px: -4 # px extra entre les dues línies (fase / completada)
black_hold_ms: 500 # pausa en negre abans d'entrar a la pròxima sala
text:
# Noms d'easing tal com els resol UiConfig::curveByName (vegeu
# `data/ui.yaml`). El text entra suau i surt accelerant.
enter_curve: outCubic
exit_curve: inCubic
# Panel translúcid pintat sota el rètol de transició (INTRO i OUTRO).
# Va centrat verticalment al centre del playfield i ocupa tota
# l'amplada del canvas. Creix d'altura amb la mateixa enter_curve del
# text i decreix amb l'exit_curve. `color` són tres canals RGB (0..255);
# `alpha` és la transparència del blend (0 = invisible, 255 = opac).
text_panel:
color: [36, 182, 170]
alpha: 128
height_px: 32
+93
View File
@@ -0,0 +1,93 @@
# 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
# 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
# fallbacks compilats; veure `readSceneConfig` a game_scene.cpp.
room_intro:
fade_in_ms: 500 # negre opac → món visible
text_delay_ms: 500 # espera amb el món net abans de sortir el text
text_enter_ms: 400 # text esquerra → centre
text_hold_ms: 2000 # text quiet al centre
text_exit_ms: 400 # text centre → fora per l'esquerra
line_gap_px: -4 # px extra entre les dues línies (piràmide / sala)
# Delays d'spawn aeri **mesurats des de la fi de la música d'INTRO**
# (callback `Audio::setOnMusicEnded`). 0 = cau al moment exacte que
# `room_intro.ogg` acaba; valors > 0 escalonen els jugadors. En mode
# single-player s'usa només `player_spawn_delay_ms`.
player_spawn_delay_ms: 0
player2_spawn_delay_ms: 300
# 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
# "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.
land_reverb: stone_hall
room_outro:
text_enter_ms: 400
text_hold_ms: 2000
text_exit_ms: 400
fade_out_ms: 500 # món visible → negre opac
line_gap_px: -4 # px extra entre les dues línies (fase / completada)
black_hold_ms: 500 # pausa en negre abans d'entrar a la pròxima sala
text:
# Noms d'easing tal com els resol UiConfig::curveByName (vegeu
# `data/config/ui.yaml`). El text entra suau i surt accelerant.
enter_curve: outCubic
exit_curve: inCubic
# Panel translúcid pintat sota el rètol de transició (INTRO i OUTRO).
# Va centrat verticalment al centre del playfield i ocupa tota
# l'amplada del canvas. Creix d'altura amb la mateixa enter_curve del
# text i decreix amb l'exit_curve. `color` són tres canals RGB (0..255);
# `alpha` és la transparència del blend (0 = invisible, 255 = opac).
text_panel:
color: [36, 182, 170]
alpha: 128
height_px: 32
# Sub-FSM de game over: dispara CONTINUE? quan tots els jugadors
# queden fora de partida durant PLAYING, fa countdown, i si ningú
# pulsa START acaba en GAME OVER + fade-to-black + transició a LOGO.
# Tots els valors són ms (excepte `continue_count_steps`, que és el
# nombre de tics visibles del countdown). Si manca el fitxer o alguna
# clau, GameScene cau als fallbacks compilats (vegeu `GameOverFsm::Config`).
game_over:
# CONTINUE? — animació d'entrada i sortida idèntica al room text.
continue_text_enter_ms: 400
# Hold del countdown: 10 s = (continue_count_start + 1) graons d'1 s.
# La sèrie visible és [continue_count_start, ..., 1, 0]; el "0" es
# manté l'últim segon abans del salt a GAME OVER.
continue_text_hold_ms: 10000
continue_text_exit_ms: 400
continue_count_start: 9
# GAME OVER — apareix sense enter anim (panel ja amunt del CONTINUE?),
# es manté un temps i marxa amb la mateixa exit anim del room text.
game_over_text_hold_ms: 1500
game_over_text_exit_ms: 400
game_over_wait_ms: 400
# Fade final cap a LOGO. `black_hold_ms` és la pausa en negre opac
# abans que el GameScene demani la transició d'escena.
fade_out_ms: 500
black_hold_ms: 500
# Velocitat (frequency ratio) de la música mentre dura el countdown
# de CONTINUE_PROMPT. 1.0 = sense canvi, 1.5 = un 50% més ràpid (i
# amb el to més agut: efecte arcade clàssic). Es restableix a 1.0 al
# sortir del prompt — tant si el jugador reviu com si entra GAME OVER.
continue_music_speed: 1.05
# Pista de música que sona durant l'estat GAME_OVER (sense loop).
# La fase de hold del text es sincronitza amb la durada real de la
# pista, així que el rètol "Fi del Joc" es manté visible mentre dura
# la música; després ve l'exit anim, el wait i el fade a LOGO.
game_over_music_track: "game_over.ogg"
# So que sona cada vegada que apareix un nou número del countdown
# (al primer cop que es mostra `continue_count_start`, a cada canvi,
# i quan apareix el 0 — però NO al saltar a GAME OVER). Buit = silenci.
countdown_sound_key: "countdown.wav"
# Llindar a partir del qual qualsevol botó de salt o atac de
# qualsevol jugador avança al següent número del countdown.
# 7 → skip habilitat mentre el número visible és 7..1 (no a 0).
# -1 = mai. Útil per accelerar la decisió un cop la urgència ja
# és evident al jugador.
skip_below_or_equal: 7
+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.
File renamed without changes.
+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
File renamed without changes.
-217
View File
@@ -1,217 +0,0 @@
# Tema de la UI: índexs de paleta, timings, fonts i flags visuals. Tota
# clau absent cau al valor compilat a Defaults::UI (veure defaults.hpp).
colors:
render_info_text: 58
render_info_shadow: 16
canvas_bg: 72
notification_text: 232
notification_bg: 64
exit_text: 71
exit_bg: 2
menu_bg: 2
menu_border: 232
menu_title: 237
menu_label: 58
menu_value: 232
menu_cursor: 71
menu_disabled: 16
hud_bg: 9
hud_label: 58
hud_value: 187
hud_flash: 237
# Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). Independents
# de notification_* perquè la fase i les notificacions són dos canals
# visuals distints: ací pots canviar el color del rètol sense tocar
# els missatges del HUD.
room_text: 160
room_text_outline: 253
timings:
notification_ms: 4000
exit_ms: 4000
fonts:
hud:
name: "8bithud"
gif: "data/fonts/8bithud.gif"
fnt: "data/fonts/8bithud.fnt"
kerning: 1
placeholder:
name: "ari-w9500-condensed-display"
gif: "data/fonts/ari-w9500-condensed-display.gif"
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
kerning: 1
# Font del rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO).
room_text:
name: "CaesarDressing-Regular"
gif: "data/fonts/CaesarDressing-Regular.gif"
fnt: "data/fonts/CaesarDressing-Regular.fnt"
kerning: 1
menu_title:
name: "ari-w9500-display"
gif: "data/fonts/ari-w9500-display.gif"
fnt: "data/fonts/ari-w9500-display.fnt"
kerning: 0
menu_items:
name: "ari-w9500-condensed-display"
gif: "data/fonts/ari-w9500-condensed-display.gif"
fnt: "data/fonts/ari-w9500-condensed-display.fnt"
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:
bg_enabled: false
entry_ms: 800 # durada de l'animació d'entrada dels tres blocs
flash_ms: 250 # durada del flash quan un jugador rep un hit
shake_ms: 300 # durada del shake que acompanya el flash
# Geometria del popup superior (Notifier). En píxels del canvas, llevat de
# slide_ms (durada en ms de l'animació d'entrada i d'eixida; constant
# independentment de la mida de la caixa). El padding vertical és asimètric a
# propòsit: padding_top > padding_bottom puja una mica el text dins la caixa.
# Corbes disponibles: linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
notifier:
padding_h: 6
padding_top: 4
padding_bottom: 3
line_gap: 1
margin_top: 3
border_px: 1
slide_ms: 300
slide_in_curve: outCubic
slide_out_curve: inCubic
# Pausa graduada de la GameScene: el món no para d'un colp, s'atura amb
# easing i torna a arrancar amb una altra corba. Els timings es poden
# afinar independentment, i les corbes permeten estils diferents entrant
# i sortint (p.ex. frenar suau i arrancar seca). Corbes disponibles:
# linear, outQuad, inQuad, inOutQuad, outCubic, inCubic.
pause:
enter_ms: 400
exit_ms: 250
enter_curve: outCubic
exit_curve: inCubic
# Sons del menú de servei. Els fitxers viuen a data/sounds/. SELECT
# acompanya navegació (amunt/avall) i back; ACCEPT acompanya modificació
# d'opció (esquerra/dreta/retorn) o entrada a un submenú.
sounds:
menu_select: "menu_select.wav"
menu_accept: "menu_accept.wav"
# Disparat quan un jugador prem START a la TITLE (primera pulsació o
# la del segon jugador dins de la finestra de STARTING).
player_start: "player_start.wav"
# Soroll/scratch sincronitzat amb la fase SCAN de LogoScene (scanline
# reveal). Es dispara al constructor de l'escena.
logo_glitch: "glitch.wav"
# Escena TITLE (placeholder visual mentre no hi haja artwork).
# `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
# Escena LOGO (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
+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
de sistema que `config.yaml`:
- Linux (XDG): `$XDG_CONFIG_HOME/jailgames/aee_2026/debug.yaml`
- Per defecte: `~/.config/jailgames/aee_2026/debug.yaml`
- Linux (XDG): `$XDG_CONFIG_HOME/jailgames/aee_arcade/debug.yaml`
- Per defecte: `~/.config/jailgames/aee_arcade/debug.yaml`
### Comportament segons build
+4 -4
View File
@@ -1,6 +1,6 @@
# 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`.
@@ -24,7 +24,7 @@ Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`.
## 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):
@@ -158,7 +158,7 @@ Al CMakeLists: `ENTITY_SOURCES` conté els fitxers core; `APP_SOURCES` afegeix `
- `make build`: OK, 0 warnings nous.
- `./build/test_entities_smoke`: **9/9 OK**.
- `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 : 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.
- `./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
+4 -4
View File
@@ -1,7 +1,7 @@
# Pla d'implementació — Menú de servei
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
referència per a implementar-ho en una sessió futura; no s'ha escrit cap codi
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
| 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()` |
| 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
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
`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.
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
hardcoded.
+3 -3
View File
@@ -7,17 +7,17 @@
<key>CFBundleDisplayName</key>
<string>Aventures en Egipte ARCADE</string>
<key>CFBundleExecutable</key>
<string>aee_2026</string>
<string>aee_arcade</string>
<key>CFBundleIconFile</key>
<string>icon</string>
<key>CFBundleIconName</key>
<string>icon</string>
<key>CFBundleIdentifier</key>
<string>org.jailgames.aee_2026</string>
<string>org.jailgames.aee_arcade</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>aee_2026</string>
<string>aee_arcade</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
@@ -1,2 +1,2 @@
// aee_2026.rc
// aee_arcade.rc
IDI_ICON1 ICON "release/icons/icon.ico"
+7
View File
@@ -113,6 +113,13 @@ void Audio::stopMusic() {
}
}
// NOLINTNEXTLINE(readability-make-member-function-const)
void Audio::setMusicSpeed(float ratio) {
if (music_enabled_) {
engine_->setMusicSpeed(ratio);
}
}
// Reprodueix un so per nom
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::playSound(const std::string& name, Group group) {
+6 -1
View File
@@ -76,11 +76,16 @@ class Audio {
void stopMusic(); // Aturar completament la música
void fadeOutMusic(int milliseconds); // Fosa de sortida de la música (muta globals de Ja)
void setOnMusicEnded(std::function<void()> callback); // Callback disparat quan la pista actual acaba de drenar (CONV-03)
// Multiplicador de velocitat de la música actual. 1.0 = normal,
// 1.5 = un 50% més ràpid (efecte "chipmunk" — també puja el to).
// Es reseteja a 1.0 implícitament a cada `playMusic`. No-op si no
// hi ha música activa.
void setMusicSpeed(float ratio);
// --- Control de sons ---
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)
// 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
// o el motor està al cap de canals amb efecte, fa fallback a playSound
// sec — l'usuari sent el so igualment, sense la cua.
+3 -3
View File
@@ -5,10 +5,10 @@
#include <cmath>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <vector>
#include "core/audio/jail_audio.hpp"
#include "utils/log.hpp"
namespace AudioEffects {
@@ -46,11 +46,11 @@ namespace AudioEffects {
} else if (spec.format == SDL_AUDIO_U8) {
bytes_per_sample = 1;
} 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;
}
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;
}
+5
View File
@@ -286,6 +286,11 @@ namespace Ja {
}
}
void Engine::setMusicSpeed(float ratio) {
if (current_music_ == nullptr || current_music_->stream == nullptr) { return; }
SDL_SetAudioStreamFrequencyRatio(current_music_->stream, ratio);
}
void Engine::pauseMusic() {
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
+8
View File
@@ -171,6 +171,14 @@ namespace Ja {
void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
[[nodiscard]] auto getMusicState() const -> MusicState;
auto setMusicVolume(float volume) -> float;
// Multiplicador de velocitat de reproducció de la música actual
// via `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal, 2.0 =
// doble velocitat. Cal saber que també puja el to (efecte
// "chipmunk") — és el comportament arcade clàssic dels comptes
// enrere. Cada `playMusic` crea un stream nou amb ratio 1.0,
// així que un canvi de track reseteja la velocitat
// implícitament. No-op si no hi ha música activa.
void setMusicSpeed(float ratio);
// Registra un callback que es disparà quan la música actual acabi de
// drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de
// stopMusic, així que el callback pot invocar playMusic sense córrer.
+4 -4
View File
@@ -1,10 +1,10 @@
#include "core/audio/sound_effects_config.hpp"
#include <iostream>
#include <string>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace {
@@ -27,7 +27,7 @@ auto SoundEffectsConfig::get() -> SoundEffectsConfig& {
void SoundEffectsConfig::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path);
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;
}
@@ -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) {
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
// 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
// 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 <array>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string_view>
#include <utility>
#include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace {
@@ -90,7 +90,7 @@ auto GamepadBindings::get() -> GamepadBindings& {
}
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ú).
switch (action) {
case Action::JUMP:
@@ -167,8 +167,7 @@ void GamepadBindings::load(const std::string& file_path) {
try {
auto yaml = fkyaml::node::deserialize(CONTENT);
if (!yaml.contains("gamepads") || !yaml["gamepads"].is_sequence()) {
std::cerr << "GamepadBindings: secció 'gamepads' absent o invàlida a "
<< file_path << '\n';
Log::error("GamepadBindings", "secció 'gamepads' absent o invàlida a {}", file_path);
return;
}
for (const auto& entry : yaml["gamepads"]) {
@@ -188,11 +187,9 @@ void GamepadBindings::load(const std::string& file_path) {
}
configs_.push_back(std::move(cfg));
}
std::cout << "GamepadBindings: " << configs_.size() << " mandos carregats de "
<< file_path << '\n';
Log::info("GamepadBindings", "{} mandos carregats de {}", configs_.size(), file_path);
} catch (const std::exception& e) {
std::cerr << "GamepadBindings: error parsejant " << file_path << ": "
<< e.what() << '\n';
Log::error("GamepadBindings", "error parsejant {}: {}", file_path, e.what());
}
}
@@ -200,7 +197,7 @@ auto GamepadBindings::save() -> bool {
if (file_path_.empty()) { return false; }
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 << "gamepads:\n";
for (const auto& cfg : configs_) {
@@ -215,7 +212,7 @@ auto GamepadBindings::save() -> bool {
std::ofstream file(file_path_, std::ios::binary);
if (!file) {
std::cerr << "GamepadBindings: no es pot escriure " << file_path_ << '\n';
Log::error("GamepadBindings", "no es pot escriure {}", file_path_);
return false;
}
file << out.str();
+12 -20
View File
@@ -1,13 +1,13 @@
#include "core/input/gamepad_manager.hpp"
#include <algorithm>
#include <iostream>
#include <iterator>
#include <string>
#include <utility>
#include "core/locale/locale.hpp"
#include "core/rendering/notification_presets.hpp"
#include "utils/log.hpp"
namespace {
@@ -94,8 +94,7 @@ void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
if (!SDL_IsGamepad(jid)) { return; }
SDL_Gamepad* pad = SDL_OpenGamepad(jid);
if (pad == nullptr) {
std::cout << "GamepadManager: SDL_OpenGamepad(" << jid
<< ") ha fallat: " << SDL_GetError() << '\n';
Log::error("GamepadManager", "SDL_OpenGamepad({}) ha fallat: {}", jid, SDL_GetError());
return;
}
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.
const char* raw_path = SDL_GetGamepadPath(pad);
std::string path = (raw_path != nullptr) ? raw_path : "";
std::cout << "GamepadManager: connectat " << name
<< (path.empty() ? "" : " [" + path + "]") << '\n';
Log::info("GamepadManager", "connectat {}{}", name, path.empty() ? std::string{} : " [" + 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,
@@ -129,7 +127,7 @@ void GamepadManager::removeGamepad(SDL_JoystickID jid) {
std::string name = std::move(it->pretty_name);
SDL_Gamepad* pad = it->pad;
if (pad != nullptr) { SDL_CloseGamepad(pad); }
std::cout << "GamepadManager: desconnectat " << name << '\n';
Log::info("GamepadManager", "desconnectat {}", name);
entries_.erase(it);
// Qualsevol assignació que apuntava a aquest mando queda amb `pad`
@@ -145,8 +143,7 @@ void GamepadManager::removeGamepad(SDL_JoystickID jid) {
void GamepadManager::init() {
if (initialized_) { return; }
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
std::cout << "GamepadManager: SDL_INIT_GAMEPAD ha fallat: "
<< SDL_GetError() << '\n';
Log::error("GamepadManager", "SDL_INIT_GAMEPAD ha fallat: {}", SDL_GetError());
return;
}
// `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());
if (ADDED < 0) {
std::cout << "GamepadManager: no s'ha pogut carregar " << db_path
<< ": " << SDL_GetError() << '\n';
Log::warn("GamepadManager", "no s'ha pogut carregar {}: {}", db_path, SDL_GetError());
} else {
std::cout << "GamepadManager: " << ADDED << " mappings carregats de " << db_path << '\n';
Log::info("GamepadManager", "{} mappings carregats de {}", ADDED, db_path);
}
int count = 0;
@@ -175,7 +171,7 @@ void GamepadManager::init() {
}
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;
}
@@ -268,8 +264,7 @@ void GamepadManager::assignToPlayer(int player_id, const std::string& name, cons
if (a.pad != nullptr) {
for (auto& other : assignments_) {
if (&other != &a && other.pad == a.pad) {
std::cout << "GamepadManager: mando " << a.name
<< " reassignat (last-wins)\n";
Log::info("GamepadManager", "mando {} reassignat (last-wins)", a.name);
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
// visible a la consola igual que les autoassignacions.
if (a.pad != nullptr) {
std::cout << "GamepadManager: assignat " << a.name
<< " → P" << (player_id + 1) << '\n';
Log::info("GamepadManager", "assignat {} → P{}", a.name, player_id + 1);
} else {
std::cout << "GamepadManager: P" << (player_id + 1) << " pendent ("
<< (a.name.empty() ? a.path : a.name) << " no connectat)\n";
Log::info("GamepadManager", "P{} pendent ({} no connectat)", player_id + 1, a.name.empty() ? a.path : a.name);
}
}
@@ -307,8 +300,7 @@ auto GamepadManager::autoAssignFreeSlots() -> std::vector<AutoAssigned> {
slot.name = e.pretty_name;
slot.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
<< " → P" << (pid + 1) << '\n';
Log::info("GamepadManager", "auto-assigned {} → P{}", e.pretty_name, pid + 1);
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 confirmació expiren alhora — si no, l'usuari podria veure el
// 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 MAX_ZOOM = 6;
@@ -161,7 +161,7 @@ namespace GlobalInputs {
// Debug helper: dispara una notificació prou llarga perquè
// `wrapToWidth` la parteixi en (almenys) dues línies. Útil per a
// 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() {
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 <fstream>
#include <iostream>
#include <string>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
auto KeyConfig::get() -> KeyConfig& {
static KeyConfig instance_;
@@ -15,7 +15,7 @@ auto KeyConfig::get() -> KeyConfig& {
void KeyConfig::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path);
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;
}
@@ -24,7 +24,7 @@ void KeyConfig::load(const std::string& file_path) {
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
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;
}
bindings_.clear();
@@ -34,14 +34,14 @@ void KeyConfig::load(const std::string& file_path) {
const auto CODE = entry["code"].get_value<std::string>();
const SDL_Scancode SC = SDL_GetScancodeFromName(CODE.c_str());
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;
}
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) {
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 <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
// 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
+5 -5
View File
@@ -1,11 +1,11 @@
#include "core/locale/locale.hpp"
#include <fstream>
#include <iostream>
#include <string>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace {
@@ -36,7 +36,7 @@ auto Locale::get() -> Locale& {
void Locale::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path);
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;
}
@@ -46,15 +46,15 @@ void Locale::load(const std::string& file_path) {
auto yaml = fkyaml::node::deserialize(begin, end);
strings_.clear();
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) {
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 it = strings_.find(key);
if (it != strings_.end()) { return it->second; }
std::cerr << "Locale: clau no trobada: " << key << '\n';
Log::warn("Locale", "clau no trobada: {}", key);
return key;
}
+33 -6
View File
@@ -50,6 +50,8 @@ namespace Effects {
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) {
@@ -63,9 +65,13 @@ namespace Effects {
burst_remaining_ms_ = std::max(0, burst_remaining_ms_ - delta_ms);
}
// 3) Spawn d'un nou burst (només durant el scan, no al flash).
// 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.
if (burst_remaining_ms_ == 0 && elapsed_ms_ < params_.duration_ms && params_.vhs_burst_per_sec > 0 && params_.vhs_burst_duration_ms > 0) {
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) {
@@ -86,6 +92,17 @@ namespace Effects {
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 {
@@ -94,8 +111,12 @@ namespace Effects {
auto ScanReveal::isFinished() const -> bool {
if (params_.duration_ms <= 0) { return true; }
// Acabat = scan al final + flash exhaurit (o sense flash).
return elapsed_ms_ >= params_.duration_ms && flash_remaining_ms_ <= 0;
// 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 {
@@ -118,15 +139,21 @@ namespace Effects {
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.
// 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 (params_.glitch_band_radius_px > 0 && DIST_TO_BAND <= params_.glitch_band_radius_px && params_.glitch_amplitude_px > 0) {
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(
+15 -2
View File
@@ -18,8 +18,11 @@ namespace Effects {
// demoscene/VHS deteriorat.
//
// Quan la banda surt per sota del logo i s'esgota `duration_ms`,
// dispara un flash blanc breu i passa a `isFinished()=true`. El
// caller decideix què fa amb el control de fase a partir d'aquí.
// 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;
@@ -43,6 +46,10 @@ namespace Effects {
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();
@@ -75,6 +82,12 @@ namespace Effects {
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_;
+6 -7
View File
@@ -1,10 +1,11 @@
#include "core/rendering/gif.hpp"
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <string>
#include "utils/log.hpp"
namespace GIF {
namespace {
@@ -111,7 +112,7 @@ namespace GIF {
while (cur_code != -1) {
out[dictionary[cur_code].len - 1] = dictionary[cur_code].byte;
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");
}
cur_code = dictionary[cur_code].prev;
@@ -155,8 +156,7 @@ namespace GIF {
if (prev > -1 && code_length < LZW_MAX_CODE_LENGTH) {
if (code > dictionary_ind) {
std::cerr << "code = " << std::hex << code
<< ", but dictionary_ind = " << dictionary_ind << '\n';
Log::error("GIF", "code = {:x}, but dictionary_ind = {}", code, 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.
if (code < 0 || static_cast<size_t>(code) >= dictionary.size()) {
std::cerr << "Invalid LZW code " << code
<< ", dictionary size " << dictionary.size() << '\n';
Log::error("GIF", "invalid LZW code {}, dictionary size {}", code, dictionary.size());
throw std::runtime_error("LZW error: invalid code encountered");
}
@@ -287,7 +286,7 @@ namespace GIF {
} else if (block_type == IMAGE_DESCRIPTOR) {
return processImageDescriptor(buffer);
} 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>{};
}
block_type = *buffer++;
@@ -9,7 +9,7 @@
// 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
// 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
// Notification::notify(text, preset). Sense preset s'usa el DEFAULT.
namespace Notification {
@@ -22,7 +22,7 @@ namespace Notification {
// 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
// data/ui.yaml i es carreguen a l'arrencada.
// data/config/ui.yaml i es carreguen a l'arrencada.
[[nodiscard]] inline auto defaultPreset() -> Preset {
return {
.text_color = UiConfig::get().notificationText(),
+3 -3
View File
@@ -6,13 +6,13 @@
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <ranges>
#include "core/rendering/canvas.hpp"
#include "core/rendering/sdl3gpu/viewport.hpp"
#include "utils/defines.hpp"
#include "utils/log.hpp"
namespace {
@@ -112,11 +112,11 @@ void Screen::createSdl() {
backend->setVSync(config_.vsync);
if (backend->init(window_, canvas_width_, canvas_height_)) {
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);
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";
@@ -1,10 +1,10 @@
#include "core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp"
#include <cstring>
#include <iostream>
#include "core/rendering/sdl3gpu/shader_factory.hpp"
#include "core/rendering/sdl3gpu/viewport.hpp"
#include "utils/log.hpp"
#ifdef __APPLE__
#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);
#endif
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 (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
return false;
@@ -57,7 +57,7 @@ namespace Rendering {
SDL_ReleaseGPUShader(device_, frag);
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;
}
@@ -2,10 +2,10 @@
#include <cstdint>
#include <cstring>
#include <iostream>
#include "core/rendering/sdl3gpu/shader_factory.hpp"
#include "core/rendering/sdl3gpu/viewport.hpp"
#include "utils/log.hpp"
#ifdef __APPLE__
#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();
device_ = SDL_CreateGPUDevice(FORMAT, false, driver);
if (device_ == nullptr) {
std::cerr << "SDL3GPUShader: SDL_CreateGPUDevice ha fallat: " << SDL_GetError() << '\n';
Log::error("SDL3GPUShader", "SDL_CreateGPUDevice ha fallat: {}", SDL_GetError());
return false;
}
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;
}
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);
#endif
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 true;
@@ -105,7 +105,7 @@ namespace Rendering {
pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &info);
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 true;
@@ -137,7 +137,7 @@ namespace Rendering {
sampler_ = SDL_CreateGPUSampler(device_, &samp_info);
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 true;
@@ -1,12 +1,12 @@
#include "core/rendering/sprite/animated_sprite.hpp"
#include <cmath>
#include <iostream>
#include <numeric>
#include <utility>
#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp"
#include "utils/log.hpp"
// El parser pure de YAML d'animacions (parseAnimationsFromYaml + helpers)
// 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); }
}
} 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;
} 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;
}
}
@@ -140,7 +140,7 @@ void AnimatedSprite::setCurrentAnimation(const std::string& name) {
if (current_animation_ == name) { return; }
auto it = animations_.find(name);
if (it == animations_.end()) {
std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n';
Log::warn("AnimatedSprite", "could not find \"{}\" animation", name);
return;
}
current_animation_ = name;
+1 -2
View File
@@ -2,7 +2,6 @@
#include <algorithm>
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <vector>
@@ -63,7 +62,7 @@ Surface::Surface(const std::string& file_path) {
palette_[i] = Rendering::OPAQUE_ALPHA | rgb[i];
}
printWithDots("Surface : ", getFileName(file_path), "[ LOADED ]");
printWithDots("Surface", getFileName(file_path), "[ LOADED ]");
}
void Surface::clear(Uint8 color) {
+3 -3
View File
@@ -3,7 +3,6 @@
#include <SDL3/SDL.h>
#include <cstddef>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
@@ -11,6 +10,7 @@
#include "core/rendering/sprite/sprite.hpp"
#include "core/rendering/surface.hpp"
#include "core/resources/resource_helper.hpp"
#include "utils/log.hpp"
#include "utils/utils.hpp"
// 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);
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));
}
@@ -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;
}
+10 -10
View File
@@ -1,12 +1,12 @@
#include "core/resources/resource_cache.hpp"
#include <algorithm>
#include <iostream>
#include <stdexcept>
#include "core/audio/jail_audio.hpp"
#include "core/resources/palette_loader.hpp"
#include "core/resources/resource_helper.hpp"
#include "utils/log.hpp"
#include "utils/utils.hpp"
// 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);
}
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) {
auto palette = PaletteLoader::loadFromFile(file_path);
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 it = std::ranges::find_if(surfaces_, [&name](const auto& s) { return s.name == name; });
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);
}
return it->surface;
@@ -58,7 +58,7 @@ namespace Resource {
auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& {
auto it = std::ranges::find_if(animations_, [&name](const auto& a) { return a.name == name; });
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);
}
return *it;
@@ -67,7 +67,7 @@ namespace Resource {
auto Cache::getPalette(const std::string& name) -> const Palette& {
auto it = std::ranges::find_if(palettes_, [&name](const auto& p) { return p.name == name; });
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);
}
return it->palette;
@@ -83,7 +83,7 @@ namespace Resource {
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)});
printWithDots("Music : ", getFileName(file_path), "[ LOADED ]");
printWithDots("Music", getFileName(file_path), "[ LOADED ]");
}
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);
}
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 it = std::ranges::find_if(musics_, [&name](const auto& m) { return m.name == name; });
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);
}
return it->music.get();
@@ -111,7 +111,7 @@ namespace Resource {
auto Cache::getSound(const std::string& name) -> Ja::Sound* {
auto it = std::ranges::find_if(sounds_, [&name](const auto& s) { return s.name == name; });
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);
}
return it->sound.get();
+8 -10
View File
@@ -1,11 +1,12 @@
#include "core/resources/resource_helper.hpp"
#include <fstream>
#include <iostream>
#include <string>
#include <string_view>
#include "core/resources/resource_pack.hpp"
#include "utils/defaults.hpp"
#include "utils/log.hpp"
namespace Resource::Helper {
@@ -26,14 +27,14 @@ namespace Resource::Helper {
// Silenciem si veníem de prefer_disk: aleshores caurem
// al pack i no és cap error real que el fitxer no estigui
// al disc.
if (verbose) { std::cerr << "Resource::Helper: cannot open " << FULL << '\n'; }
if (verbose) { Log::error("Resource::Helper", "cannot open {}", FULL); }
return {};
}
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<std::uint8_t> data(static_cast<std::size_t>(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 data;
@@ -51,14 +52,11 @@ namespace Resource::Helper {
g_pack_loaded = g_pack.loadPack(pack_file);
if (g_pack_loaded) {
std::cout << "Resource::Helper: pack loaded (" << g_pack.getResourceCount()
<< " entries) from " << pack_file << '\n';
Log::info("Resource::Helper", "pack loaded ({} entries) from {}", g_pack.getResourceCount(), pack_file);
} else if (enable_fallback) {
std::cout << "Resource::Helper: no pack at " << pack_file
<< " — using filesystem fallback\n";
Log::info("Resource::Helper", "no pack at {} — using filesystem fallback", pack_file);
} else {
std::cerr << "Resource::Helper: FATAL — no pack at " << pack_file
<< " and fallback disabled\n";
Log::critical("Resource::Helper", "FATAL — no pack at {} and fallback disabled", pack_file);
return false;
}
return true;
@@ -92,7 +90,7 @@ namespace Resource::Helper {
// carpeta `data/`).
const auto FROM_PACK = [&filepath]() -> std::vector<std::uint8_t> {
if (!g_pack_loaded) { return {}; }
constexpr std::string_view PREFIX = "data/";
const std::string_view PREFIX{Defaults::Paths::DATA_ROOT};
std::string key = filepath;
if (key.starts_with(PREFIX)) { key.erase(0, PREFIX.size()); }
if (g_pack.hasResource(key)) { return g_pack.getResource(key); }
+11 -10
View File
@@ -4,11 +4,12 @@
#include <array>
#include <filesystem>
#include <fstream>
#include <iostream>
#include "utils/log.hpp"
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 {
constexpr const char* MAGIC = "AE26";
@@ -40,14 +41,14 @@ namespace Resource {
std::array<char, 4> header{};
file.read(header.data(), 4);
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;
}
std::uint32_t version = 0;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != VERSION) {
std::cerr << "Resource::Pack: unsupported pack version: " << version << '\n';
Log::error("Resource::Pack", "unsupported pack version: {}", version);
return false;
}
@@ -86,7 +87,7 @@ namespace Resource {
auto Pack::savePack(const std::string& pack_file) const -> bool {
std::ofstream file(pack_file, std::ios::binary);
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;
}
@@ -120,7 +121,7 @@ namespace Resource {
auto Pack::addFile(const std::string& filename, const std::string& filepath) -> bool {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Resource::Pack: could not open file: " << filepath << '\n';
Log::error("Resource::Pack", "could not open file: {}", filepath);
return false;
}
@@ -129,7 +130,7 @@ namespace Resource {
std::vector<std::uint8_t> file_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;
}
@@ -146,7 +147,7 @@ namespace Resource {
auto Pack::addDirectory(const std::string& directory) -> bool {
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;
}
@@ -169,7 +170,7 @@ namespace Resource {
const PackEntry& entry = it->second;
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 {};
}
@@ -179,7 +180,7 @@ namespace Resource {
std::uint32_t checksum = calculateChecksum(result);
if (checksum != entry.checksum) {
std::cerr << "Resource::Pack: checksum mismatch for: " << filename << '\n';
Log::error("Resource::Pack", "checksum mismatch for: {}", filename);
}
return result;
}
+68 -41
View File
@@ -4,10 +4,8 @@
#include <algorithm>
#include <any>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
@@ -31,6 +29,7 @@
#include "core/system/event_buffer.hpp"
#include "core/ui/ui_config.hpp"
#include "game/debug_config.hpp"
#include "game/gameplay_config.hpp"
#ifdef _DEBUG
#include "game/editor/editor_config.hpp"
#endif
@@ -42,10 +41,11 @@
#include "game/scenes/title_scene.hpp"
#include "game/ui/service_menu.hpp"
#include "utils/defaults.hpp"
#include "utils/log.hpp"
Director::Director() {
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
@@ -66,12 +66,13 @@ Director::Director() {
constexpr bool ENABLE_FALLBACK = false;
#endif
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);
}
// Prepara la carpeta de sistema (~/.config/jailgames/aee_2026/) i carrega
// les opcions; si no existeix config.yaml el crea amb els defaults.
// Prepara la carpeta de sistema (per plataforma: %APPDATA% a Windows,
// ~/Library/Application Support a macOS, ~/.config a Linux) i carrega les
// opcions; si no existeix config.yaml el crea amb els defaults.
createSystemFolder();
// Gamepad: descobreix mandos presents silenciosament. Cal fer-ho ABANS
@@ -85,7 +86,7 @@ Director::Director() {
Options::setConfigFile(system_folder_ + "config.yaml");
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`
@@ -93,16 +94,21 @@ Director::Director() {
// el patró de coffee_crisis_arcade_edition (`gamepad_configs.json`).
GamepadBindings::get().load(system_folder_ + "gamepad_configs.yaml");
// Tweaks de jugabilitat (látigo travessa sarcs, friendly fire, col·lisió
// entre jugadors). Fitxer escrivible des del menú de servei (F9). Si
// no existeix, GameplayConfig::load el crea amb els defaults.
GameplayConfig::get().load(system_folder_ + "gameplay.yaml");
// Carrega els presets de PostFX des de shaders/postfx.yaml; si no existeix
// es crea amb presets per defecte (off, crt, ntsc).
Options::setPostFXFile(system_folder_ + Defaults::Video::POSTFX_FILE);
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).
Options::setCrtPiFile(system_folder_ + Defaults::Video::CRTPI_FILE);
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
@@ -118,22 +124,29 @@ Director::Director() {
Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale() + ".yaml");
// 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
// data/ui.yaml cau al valor compilat a Defaults::UI.
UiConfig::get().load("data/ui.yaml");
// data/config/ui.yaml cau al valor compilat a Defaults::UI.
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
// Tema visual de l'editor de tilemap (Debug-only). Claus absents
// cauen als valors compilats a Defaults::Editor.
EditorConfig::get().load("data/editor/editor.yaml");
EditorConfig::get().load(Defaults::Paths::EDITOR_YAML);
#endif
// Recursos compartits d'UI (paleta + fonts). Es carreguen abans que els
// overlays perquè tots els reaprofiten sense duplicar la lògica. Els
// noms de font viuen al fitxer data/ui.yaml via UiConfig.
Resource::Cache::get().loadPalette("ui", "data/palettes/uzebox.pal");
// noms de font viuen al fitxer data/config/ui.yaml via UiConfig.
Resource::Cache::get().loadPalette("ui", Defaults::Paths::UI_PALETTE);
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().getSurface(HUD_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
@@ -152,33 +165,36 @@ Director::Director() {
// 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;
// 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("playing.ogg", "data/music/playing.ogg");
Resource::Cache::get().loadMusic("room_outro.ogg", "data/music/room_outro.ogg");
Resource::Cache::get().loadMusic("room_intro.ogg", Defaults::Paths::MUSIC_ROOM_INTRO);
Resource::Cache::get().loadMusic("playing.ogg", Defaults::Paths::MUSIC_PLAYING);
Resource::Cache::get().loadMusic("room_outro.ogg", Defaults::Paths::MUSIC_ROOM_OUTRO);
// Pista de game over: pre-load igual que la resta perquè el playMusic
// del moment en què es dispara la sub-FSM no facti disc.
Resource::Cache::get().loadMusic("game_over.ogg", Defaults::Paths::MUSIC_GAME_OVER);
// Música de la TITLE: arrenca al constructor de l'escena i es fa
// 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
// dispara via Audio::Group::INTERFACE.
const auto& menu_select = UiConfig::get().menuSelectSound();
const auto& menu_accept = UiConfig::get().menuAcceptSound();
Resource::Cache::get().loadSound(menu_select, "data/sounds/" + menu_select);
Resource::Cache::get().loadSound(menu_accept, "data/sounds/" + menu_accept);
Resource::Cache::get().loadSound(menu_select, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + menu_select);
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
// prem START (vore enterStarting i el reset del segon jugador).
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, "data/sounds/" + logo_glitch);
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
// perquè els primers playSoundWith* (al començament del joc) ja
// 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
// explícitament a ~Director abans que Resource::Cache (Meyers) buide els
@@ -216,7 +232,7 @@ Director::Director() {
// 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
// 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) {
const std::string KEY = font.name + ".gif";
Resource::Cache::get().loadSurface(KEY, font.gif);
@@ -233,7 +249,7 @@ Director::Director() {
// sobreescrivim amb la "ui" perquè el GIF ja porta els colors finals
// del logo).
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);
mini_logo_surface_ = mini_logo_surface;
@@ -241,20 +257,20 @@ Director::Director() {
// 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, "data/logo/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, "data/title/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
// la seua paleta nadiua perquè el GIF ja porta els colors finals.
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);
title_bg_surface_ = title_bg_surface;
ServiceMenu::get().init(menu_title_text, menu_items_text, ui_palette);
@@ -288,8 +304,7 @@ Director::Director() {
const auto START = DebugConfig::StartScene::LOGO;
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
#endif
std::cout << "Director: start_scene=" << DebugConfig::toString(START)
<< " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n';
Log::info("Director", "start_scene={} starting_players={}", DebugConfig::toString(START), DebugConfig::toString(STARTING_PLAYERS));
switch (START) {
case DebugConfig::StartScene::LOGO:
@@ -326,25 +341,37 @@ Director::~Director() {
}
void Director::createSystemFolder() {
// Volem la config a $XDG_CONFIG_HOME/jailgames/aee_2026/ (defecte
// ~/.config/jailgames/aee_2026/). SDL_GetPrefPath apunta a XDG_DATA_HOME
// (.local/share), que és el lloc per a dades, no per a configuració.
const char* xdg = std::getenv("XDG_CONFIG_HOME");
// Carpeta de configuració per plataforma:
// - Windows: %APPDATA%\jailgames\aee_arcade\
// - macOS: ~/Library/Application Support/jailgames/aee_arcade/
// - Linux: $XDG_CONFIG_HOME/jailgames/aee_arcade/ (defecte
// ~/.config/jailgames/aee_arcade/)
// No fem servir SDL_GetPrefPath: a Linux apunta a XDG_DATA_HOME
// (~/.local/share), que és per a dades, no per a configuració.
std::string base;
#ifdef _WIN32
const char* appdata = std::getenv("APPDATA");
base = (appdata != nullptr && *appdata != '\0') ? std::string(appdata) : ".";
#elif defined(__APPLE__)
const char* home = std::getenv("HOME");
base = (home != nullptr) ? std::string(home) + "/Library/Application Support" : ".";
#else
const char* xdg = std::getenv("XDG_CONFIG_HOME");
if (xdg != nullptr && *xdg != '\0') {
base = xdg;
} else {
const char* home = std::getenv("HOME");
base = (home != nullptr) ? std::string(home) + "/.config" : ".";
}
system_folder_ = base + "/jailgames/aee_2026/";
#endif
system_folder_ = base + "/jailgames/aee_arcade/";
std::error_code error;
std::filesystem::create_directories(system_folder_, error);
// Subcarpeta per als presets de shaders. std::filesystem és tolerant si ja existeix.
std::filesystem::create_directories(system_folder_ + "shaders/", 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());
}
}
@@ -426,7 +453,7 @@ void Director::switchSceneIfRequested() {
// 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()) {
std::cout << "Director: scene transition (menu) → " << toString(menu_req->next) << '\n';
Log::info("Director", "scene transition (menu) → {}", toString(menu_req->next));
current_scene_ = buildScene(menu_req->next, menu_req->params);
return;
}
@@ -438,10 +465,10 @@ void Director::switchSceneIfRequested() {
if (!request.has_value()) { return; }
try {
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);
} 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");
}
}
+23 -6
View File
@@ -1,6 +1,5 @@
#include "core/ui/ui_config.hpp"
#include <iostream>
#include <string>
#include <string_view>
#include <unordered_map>
@@ -9,6 +8,7 @@
#include "external/fkyaml_node.hpp"
#include "utils/defaults.hpp"
#include "utils/easing.hpp"
#include "utils/log.hpp"
namespace {
@@ -27,7 +27,7 @@ namespace {
if (!node.contains(key)) { return; }
const int VALUE = node[key].get_value<int>();
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;
}
dst = static_cast<Uint8>(VALUE);
@@ -131,6 +131,7 @@ UiConfig::UiConfig()
},
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,
@@ -143,6 +144,13 @@ UiConfig::UiConfig()
.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_accept_sound_(Defaults::UI::MENU_ACCEPT_SOUND),
player_start_sound_(Defaults::UI::PLAYER_START_SOUND),
@@ -175,7 +183,7 @@ auto UiConfig::curveByName(const std::string& name) -> CurveFn {
const auto& reg = curveRegistry();
const auto IT = reg.find(name);
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 IT->second;
@@ -184,7 +192,7 @@ auto UiConfig::curveByName(const std::string& name) -> CurveFn {
void UiConfig::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path);
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;
}
@@ -301,6 +309,7 @@ void UiConfig::load(const std::string& file_path) {
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);
@@ -313,6 +322,14 @@ void UiConfig::load(const std::string& file_path) {
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")) {
@@ -328,8 +345,8 @@ void UiConfig::load(const std::string& file_path) {
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) {
std::cerr << "UiConfig: error parsejant " << file_path << ": " << e.what() << '\n';
Log::error("UiConfig", "error parsejant {}: {}", file_path, e.what());
}
}
+22 -5
View File
@@ -5,7 +5,7 @@
#include <string>
// 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
// 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].
// 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`.
using CurveFn = auto (*)(float) -> float;
// 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
// velocitat, perquè una caixa més gran no triga més a entrar/eixir.
struct Notifier {
@@ -124,9 +124,12 @@ class UiConfig {
[[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; }
// Escena LOGO — paràmetres del scanline reveal del logo de l'editora.
// Tot s'aplica només a la fase SCAN (substitueix l'antic FADE_IN).
// `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;
@@ -142,6 +145,19 @@ class UiConfig {
[[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
// Easing::linear com a fallback.
[[nodiscard]] auto pauseEnterCurve() const -> CurveFn { return pause_enter_curve_; }
@@ -154,7 +170,7 @@ class UiConfig {
[[nodiscard]] static auto curveByName(const std::string& name) -> CurveFn;
// 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 menuAcceptSound() const -> const std::string& { return menu_accept_sound_; }
[[nodiscard]] auto playerStartSound() const -> const std::string& { return player_start_sound_; }
@@ -229,6 +245,7 @@ class UiConfig {
TitleIntro title_intro_;
TitleHeatHaze title_heat_haze_;
LogoScan logo_scan_;
LogoTimeline logo_timeline_;
std::string menu_select_sound_;
std::string menu_accept_sound_;
+10 -10
View File
@@ -3,11 +3,11 @@
#include <exception>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace DebugConfig {
@@ -53,12 +53,12 @@ namespace DebugConfig {
if (!yaml.contains("start_scene")) { return StartScene::GAME; }
const auto RAW = yaml["start_scene"].get_value<std::string>();
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 fromName(RAW);
} 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;
}
}
@@ -72,7 +72,7 @@ namespace DebugConfig {
}
const auto CONTENT = readFile(file_path);
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 parseStartScene(CONTENT);
@@ -85,12 +85,12 @@ namespace DebugConfig {
if (!yaml.contains("starting_players")) { return StartingPlayers::BOTH; }
const auto RAW = yaml["starting_players"].get_value<std::string>();
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 playersFromName(RAW);
} 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;
}
}
@@ -110,7 +110,7 @@ namespace DebugConfig {
if (!yaml.contains("starting_level")) { return std::nullopt; }
const auto& sl = yaml["starting_level"];
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 StartingLevel{
@@ -118,7 +118,7 @@ namespace DebugConfig {
.room = sl["room"].get_value<int>(),
};
} 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;
}
}
@@ -162,7 +162,7 @@ namespace DebugConfig {
if (std::filesystem::exists(file_path, error)) { return; }
std::ofstream file(file_path);
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;
}
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 << "start_scene: GAME\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
+4 -3
View File
@@ -5,9 +5,10 @@
#include <string>
// Configuració personal de desenvolupament, carregada de `debug.yaml` dins la
// carpeta de sistema (`$XDG_CONFIG_HOME/jailgames/aee_2026/debug.yaml`). No es
// 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.
// carpeta de sistema del joc (vegeu `Director::createSystemFolder` per la
// ubicació real per plataforma). No es 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.
namespace DebugConfig {
enum class StartScene : std::uint8_t { LOGO,
+1 -1
View File
@@ -234,7 +234,7 @@ void Editor::ensureEditableEntities() {
// llança si no troba — provem primer i, en cas de miss, carreguem.
const auto REL = sp["path"].get_value<std::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 += '/';
full += REL;
+5 -5
View File
@@ -3,13 +3,13 @@
#ifdef _DEBUG
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <string>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/defaults.hpp"
#include "utils/log.hpp"
namespace {
@@ -34,7 +34,7 @@ namespace {
if (!s.empty() && s.front() == '#') { s.erase(0, 1); }
dst = static_cast<std::uint32_t>(std::stoul(s, nullptr, 0));
} 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) {
auto bytes = Resource::Helper::loadFile(file_path);
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;
}
try {
@@ -76,9 +76,9 @@ void EditorConfig::load(const std::string& file_path) {
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) {
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>
// 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).
class EditorConfig {
public:
+46 -5
View File
@@ -8,11 +8,14 @@
#include <string>
#include <utility>
#include "core/audio/audio.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/entities/entity_factory.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/entities/event_bus.hpp"
#include "game/entities/player.hpp"
#include "game/map/map.hpp"
#include "utils/defaults.hpp"
namespace {
@@ -133,6 +136,14 @@ Mummy::Mummy(const fkyaml::node& yaml)
if (h.contains("whippable")) {
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);
}
}
}
@@ -211,9 +222,24 @@ void Mummy::step(float delta_time) {
const float DX = dir_x_ * speed_ * delta_time;
const float DY = dir_y_ * speed_ * delta_time;
// Si no té direcció (p.ex. YAML sense `initial_direction`), el frame és
// no-op fins que algú la injecte amb `setDirection`.
if (DX == 0.0F && DY == 0.0F) { return; }
// Sense direcció: pot ser un setup degenerat (YAML sense
// `initial_direction`) o el residu d'un HURT que va sortir amb
// `chooseHurtDirection() == NONE` (totes les direccions provades
// bloquejades). En aquest segon cas la mòmia quedaria eternament quieta
// — `setDirection(0,0)` a `enterHurt` no toca `anim_`, així que es
// veuria animant-se sense moure's. Re-decidim com a una intersecció:
// si trobem alguna perpendicular vàlida, la prenem; si no, mitja volta.
// Si tampoc, mantenim (0,0) i tornem a provar el proper frame —
// pràcticament impossible amb la geometria actual, però no enganxa.
if (DX == 0.0F && DY == 0.0F) {
auto pools = collectDirectionPools(speed_ * delta_time, /*avoid_blocked=*/true);
if (!pools.perp.empty()) {
applyChoice(chooseDirection(pools.perp));
} else if (!pools.uturn.empty()) {
applyChoice(chooseDirection(pools.uturn));
}
return;
}
// Branca creuament: si aquest frame travessa la línia central d'un
// corredor perpendicular, amarra la caixa al centre del creuament i
@@ -511,6 +537,9 @@ void Mummy::enterHurt(Direction flee_dir) {
state_ = State::HURT;
hurt_remaining_s_ = hurt_config_.duration_s;
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);
if (CHOSEN == Direction::NONE) {
@@ -598,8 +627,20 @@ void Mummy::hurtStep(float delta_time) {
return;
}
// Topa amb mur o sarc: surt del HURT immediatament. La IA normal
// re-decideix al pròxim step (no fem U-turn ni re-orientació aquí).
// Topa amb mur del playfield: amarra la BOX a la vora exacta abans de
// transitar a ROAMING. Sense aquest snap, el drift sub-pixel acumulat a
// la cursa (DX = -hurt_speed·dt rarament és enter al joc real, on dt ve
// de SDL_GetTicks) deixaria la BOX a un offset de 0.x px dins de
// l'última cel·la. Com que la BOX és 16×16 i els passadissos també ho
// són, qualsevol drift fa que la cantonada sobresurti cap als sarcs
// adjacents — i el `chooseHurtDirection` del proper fueteig classifica
// les perpendiculars com a bloquejades per micro-overlap, retornant
// NONE i deixant la mòmia eternament quieta. Sarc-collision durant HURT
// és un cas molt menys habitual (la cursa va sempre per corredor recte);
// si arriba a passar, l'snap al playfield no l'arregla però la branca
// de recovery a `step()` el cobreix.
if (!Map::insidePlayfield(CANDIDATE)) { snapToPlayfieldEdge(DX, DY); }
state_ = State::ROAMING;
hurt_remaining_s_ = 0.0F;
}
+2
View File
@@ -3,6 +3,7 @@
#include <climits>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "external/fkyaml_node.hpp"
@@ -35,6 +36,7 @@ class Mummy : public DynamicHazard {
struct HurtConfig {
float duration_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);
+15 -1
View File
@@ -9,6 +9,7 @@
#include "game/entities/entity_factory.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/entities/player.hpp"
#include "game/gameplay_config.hpp"
#include "game/map/map.hpp"
namespace {
@@ -174,9 +175,16 @@ void WhipAttack::computeDamageRect(Direction dir, float spawn_x, float spawn_y,
// d'atac, fins a topar amb un mur o sarc, o esgotar `range_px`.
// L'amplada perpendicular és `tile_size` (els passadissos del joc
// tenen exactament aquesta mida i l'atac és lateral).
//
// Si el tweak `whip.damage_pierces_walls` està actiu, la hitbox
// ignora els sarcòfags (passa a través del bloc complet) però
// continua respectant el `insidePlayfield`: fora de la sala no hi
// ha res a colpejar i ampliar-ho generaria visuals/edge cases
// estranys. El mur extern, doncs, sempre retalla.
const auto TILE = static_cast<float>(Map::tile_size);
const int MAX_PX = static_cast<int>(std::max(0.0F, range_px));
const auto MAX = static_cast<float>(MAX_PX);
const bool PIERCES = GameplayConfig::get().whipDamagePiercesWalls();
float length = 0.0F;
auto try_probe = [&](float l) -> bool {
@@ -198,7 +206,9 @@ void WhipAttack::computeDamageRect(Direction dir, float spawn_x, float spawn_y,
probe = {.x = spawn_x, .y = spawn_y + (MAX - l), .w = TILE, .h = l};
break;
}
return Map::insidePlayfield(probe) && !Map::collides(probe);
if (!Map::insidePlayfield(probe)) { return false; }
if (PIERCES) { return true; }
return !Map::collides(probe);
};
for (int i = 1; i <= MAX_PX; ++i) {
@@ -311,6 +321,10 @@ namespace {
WhipAttack::Direction dir,
std::unordered_set<void*>& already_hit) {
if (p == owner || !playerIsTargetable(p)) { return; }
// Tweak `combat.friendly_fire`: si està desactivat, els altres
// jugadors són immunes al látigo aliat. L'owner ja queda exclòs
// per la primera guarda; aquest filtre cobreix només cross-player.
if (!GameplayConfig::get().friendlyFire()) { return; }
if (already_hit.contains(static_cast<void*>(p))) { return; }
const auto BOX = p->getBaseBox();
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
+2 -1
View File
@@ -6,6 +6,7 @@
#include "core/resources/resource_helper.hpp"
#include "game/entities/entity.hpp"
#include "game/entities/entity_factory.hpp"
#include "utils/defaults.hpp"
namespace EntityLoader {
@@ -20,7 +21,7 @@ namespace EntityLoader {
}
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
@@ -5,7 +5,6 @@
// stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer.
#include <filesystem>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
@@ -21,6 +20,8 @@
#include "game/entities/entity_manager.hpp"
#include "game/entities/player.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
// `Surface`. Les entitats exposen `getSurface()` + `getPixelRect()` i el
@@ -124,7 +125,7 @@ namespace {
if (s.contains("path")) {
const auto REL = s["path"].get_value<std::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;
}
if (s.contains("size")) {
@@ -236,12 +237,12 @@ namespace {
try {
tmpl = EntityLoader::loadTemplate(type);
} catch (const std::exception& e) {
std::cerr << "EntityManager: " << e.what() << '\n';
Log::error("EntityManager", "{}", e.what());
return nullptr;
}
auto entity = EntityFactory::instance().create(type, tmpl);
if (entity == nullptr) {
std::cerr << "EntityManager: tipus desconegut '" << type << "'\n";
Log::error("EntityManager", "tipus desconegut '{}'", type);
return nullptr;
}
sprite_out = readSpriteInfo(tmpl, type);
@@ -266,7 +267,7 @@ namespace {
namespace {
auto placeSarcophagus(EntityManager& manager, std::unique_ptr<Entity> entity, const SpriteInfo& sprite, const fkyaml::node& spawn) -> bool {
if (!spawn.contains("sarc_position")) {
std::cerr << "EntityManager: sarcophagus sense sarc_position\n";
Log::error("EntityManager", "sarcophagus sense sarc_position");
return false;
}
const auto& sp = spawn["sarc_position"];
@@ -303,7 +304,7 @@ namespace {
// un `Sarcophagus`. Usant el retorn de `release` al ctor del nou
// `unique_ptr` evitem el lint `bugprone-unused-return-value`.
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;
}
std::unique_ptr<Sarcophagus> sarc(static_cast<Sarcophagus*>(entity.release()));
@@ -322,7 +323,7 @@ namespace {
// queden tapats — comportament intencional.
auto placeDoor(EntityManager& manager, std::unique_ptr<Entity> entity, const SpriteInfo& sprite) -> bool {
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;
}
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 {
if (!spawn.contains("position")) {
std::cerr << "EntityManager: " << type << " sense position\n";
Log::error("EntityManager", "{} sense position", type);
return false;
}
const auto& p = spawn["position"];
@@ -443,10 +444,8 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
++decal_broadcast;
}
if (decal_broadcast > 0) {
std::cout << "EntityManager: " << decal_broadcast
<< " tile_painted broadcast inicial (decals de sòl)\n";
Log::info("EntityManager", "{} tile_painted broadcast inicial (decals de sòl)", decal_broadcast);
}
std::cout << "EntityManager: " << LOADED << " entitats carregades des de "
<< level_yaml_path << '\n';
Log::info("EntityManager", "{} entitats carregades des de {}", LOADED, level_yaml_path);
}
+45 -10
View File
@@ -22,6 +22,7 @@
#include "game/entities/entity.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/entities/event_bus.hpp"
#include "game/gameplay_config.hpp"
#include "game/options.hpp"
#include "utils/defaults.hpp"
@@ -179,16 +180,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("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>(); }
// Sons opcionals: el nom del fitxer (relatiu a data/sounds/) val tant
// de clau al Resource::Cache com de basename per a la càrrega. Si el
// camp manca, la clau queda buida i `playSound` no es crida.
// Sons opcionals: el nom del fitxer (relatiu a Defaults::Paths::AUDIO_SOUNDS_DIR)
// val tant de clau al Resource::Cache com de basename per a la càrrega.
// Si el camp manca, la clau queda buida i `playSound` no es crida.
if (j.contains("sound")) {
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")) {
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 +202,7 @@ void Player::readHurtBlock(const fkyaml::node& yaml) {
// So d'entrada a HURT — mateix patró que `jump.sound` i `whip.sound`.
if (h.contains("sound")) {
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 +221,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("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
// 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")) {
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 +241,7 @@ void Player::readWhipBlock(const fkyaml::node& yaml) {
// clau al cache i de path. Si manca, queda buit i no es dispara.
if (w.contains("sound")) {
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);
}
}
@@ -413,6 +415,10 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
// al check actor↔actor més avall via el predicat `isOutOfPlay`.
if (isOutOfPlay()) { return false; }
if (map_.collides(box)) { return true; }
// Tweak `players.collide_with_each_other`: si està desactivat, els
// jugadors es travessen lliurement. La col·lisió contra el món
// (murs/sarcs) ja s'ha avaluat dalt; saltem només la part actor↔actor.
if (!GameplayConfig::get().playersCollide()) { return false; }
if (all_players_ == nullptr) { return false; }
// Per al contacte actor-actor usem la hit-box petita a banda i banda:
// projectem la hit-box des de la posició base proposada i la comparem
@@ -1026,6 +1032,12 @@ void Player::resolveLandingOverlap() {
// canvi, qualsevol jugador amb el qual solape entra en HURT i
// surt projectat. Així el solapament inicial es resol per moviment
// del solapat, no per desplaçament forçat al lloc.
//
// Tweak `players.collide_with_each_other`: si està desactivat, el
// saltador travessa l'altre net en lloc de projectar-lo. Aquest
// camí és independent de `collidesWithWorld`, així que cal aturar-lo
// explícitament aquí.
if (!GameplayConfig::get().playersCollide()) { return; }
if (all_players_ == nullptr) { return; }
for (Player* other : *all_players_) {
if (other == nullptr || other == this) { continue; }
@@ -1258,11 +1270,30 @@ void Player::applyDyingMovement(float delta_time) {
}
}
void Player::resetForJoin() {
lives_remaining_ = death_config.initial_lives;
}
void Player::respawnFromDying() {
// Vides: decreix sempre. Pot anar a negatius — no hi ha game over.
// Vides: decreix una unitat. Si el comptador arriba a 0 el jugador
// surt de partida (DISABLED) i la GameScene/HUD reaccionen via bus
// — això habilita el flux de "join in progress" (l'altre jugador
// pot pulsar START per entrar) i obrirà el camí al CONTINUE? +
// GAME OVER en una fase posterior.
--lives_remaining_;
death_animation_complete_ = false;
respawn_remaining_ms_ = 0.0F;
if (lives_remaining_ <= 0) {
disable();
if (bus_ != nullptr) {
bus_->publish(Event{
.name = "player_disabled",
.payload = std::monostate{},
.player_id = id_,
});
}
return;
}
// Cinemàtica idèntica a la d'INTRO de sala: el jugador cau des de
// dalt fins al passadís d'spawn (`spawnFromAir` reseteja primer i
// després posa AIRBORNE amb l'altura derivada de la geometria).
@@ -1736,6 +1767,10 @@ auto Player::tryPushSnapBlockers(
float (Player::*get_pos)() const,
void (Player::*set_pos)(float)) -> bool {
if (all_players_ == nullptr) { return false; }
// Si el tweak de col·lisió entre jugadors està desactivat, mai hi
// ha res a empentar — els jugadors són intangibles entre si i el
// snap pot procedir lliurement sense desplaçar ningú.
if (!GameplayConfig::get().playersCollide()) { return false; }
// Blockers = jugadors que ara mateix solapen la nostra hit-box (ja
// amb el snap aplicat). Mateix criteri actor-actor que `collidesWithWorld`.
+34 -14
View File
@@ -91,7 +91,7 @@ class Player : public AnimatedSprite {
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
// 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).
@@ -127,6 +127,11 @@ class Player : public AnimatedSprite {
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
static void loadFrom(const std::string& yaml_path);
// Vides amb què arrenca un jugador (camp `lives:` del YAML del
// jugador). El consumeix el HUD per inicialitzar el comptador del
// marcador i `Player::resetForJoin` per a re-entrades mid-partida.
[[nodiscard]] static auto getInitialLives() -> int { return death_config.initial_lives; }
Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
// Update en una sola crida: manté l'API anterior per als tests i
@@ -296,8 +301,11 @@ class Player : public AnimatedSprite {
// `death_spinning`, posició congelada, input ignorat. Idempotent —
// si ja està DYING, EXITED o DISABLED, és no-op (un segon toc de
// momia mentre la roda no la reinicia). Quan l'animació acaba i
// `respawn_delay_ms` expira, `respawn(spawn_x_, spawn_y_)` torna a
// GROUNDED i `lives_remaining_` decreix en 1 (pot anar a negatius).
// `respawn_delay_ms` expira, `respawnFromDying()` decrementa
// `lives_remaining_`: si encara queden vides, fa `spawnFromAir`
// al spawn point; si arriba a 0, fa `disable()` i publica al bus
// `player_disabled` perquè GameScene/HUD reaccionin (slot fora de
// partida, esperant un nou START per re-entrar via "join in progress").
void enterDying(DeathCause cause = DeathCause::GENERIC);
// Coordenades del spawn de respawn (cantó superior-esquerra del
@@ -309,9 +317,11 @@ class Player : public AnimatedSprite {
spawn_y_ = y;
}
// Comptador d'intents (estil Mario, NO HP). Decreix a cada respawn;
// no es clampa a 0 — permet anar a negatius perquè avui no hi ha
// game over. Públic per a HUD i tests.
// Comptador d'intents (estil Mario, NO HP). Decreix a cada respawn
// post-mort. En arribar a 0 el jugador passa a DISABLED i publica
// `player_disabled` al bus; pot tornar a partida via "join in
// progress" (GameScene::joinPlayer crida `resetForJoin`). Públic
// per a HUD i tests.
[[nodiscard]] auto getLivesRemaining() const -> int { return lives_remaining_; }
// Activa la immunitat per `duration_ms` ms. Mentre `isImmune()` és
@@ -342,6 +352,14 @@ class Player : public AnimatedSprite {
// sala usa `disable()` + `spawnFromAir()`.
void respawn(float x, float y);
// Restaura el comptador de vides al valor inicial configurat
// (`death_config.initial_lives`). Cridat per la GameScene quan un
// slot inactiu pulsa START per entrar mid-partida (regla 6 de la
// FSM de joc): el jugador entra amb les vides plenes. La resta de
// l'estat (posició, animació, flags) la reseteja `spawnFromAir`
// que GameScene crida tot seguit.
void resetForJoin();
// Posa el jugador en el slot inactiu (DISABLED). Idempotent: la
// segona crida és no-op. Mateix patró que `exit()` per simetria.
// No publica res al bus — el GameScene decideix quan disparar el
@@ -362,7 +380,7 @@ class Player : public AnimatedSprite {
// al respawn post-mort: mateixa entrada cinemàtica.
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
// d'INTRO de sala: GameScene crida aquesta funció just després de
// `spawnFromAir`, i el primer LAND consumeix el preset (queda buit
@@ -529,7 +547,7 @@ class Player : public AnimatedSprite {
// gestiona `intended_vx_/vy_` escalats pel resolver.
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`
// (cridada per GameScene al spawn aeri d'INTRO) i el consumeix
// `integrateJump` quan el jugador toca terra. Cap altre punt
@@ -636,7 +654,7 @@ class Player : public AnimatedSprite {
struct HurtConfig {
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.
// 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
// de clau al Resource::Cache i de path. Buit o absent = entrada
// silenciosa.
@@ -660,7 +678,7 @@ class Player : public AnimatedSprite {
int cooldown_ms{0};
int damage{0};
// 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.
std::string sound_key;
};
@@ -697,7 +715,7 @@ class Player : public AnimatedSprite {
float duration_ms{0.0F};
int blink_period_ms{0};
// 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
// pot ser confusa, però no és incorrecte).
std::string palette_key;
@@ -706,7 +724,7 @@ class Player : public AnimatedSprite {
// Llig el bloc `immunity:` del YAML i n'omple `immunity_config`. Si
// 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.
static void readImmunityBlock(const fkyaml::node& yaml);
@@ -829,8 +847,10 @@ class Player : public AnimatedSprite {
// Comptador d'intents (estil Mario). Inicialitzat a `death_config.
// initial_lives` al ctor; decreix una vegada per cada respawn
// post-mort (no per respawns de canvi de sala). Pot anar a negatius:
// avui no hi ha game over.
// post-mort (no per respawns de canvi de sala). En arribar a 0,
// `respawnFromDying` posa el jugador a DISABLED i publica
// `player_disabled` al bus. `resetForJoin()` el restaura al valor
// inicial quan el jugador re-entra mid-partida.
int lives_remaining_{0};
// Coordenades del spawn de respawn (cantó superior-esquerra del
+163
View File
@@ -0,0 +1,163 @@
#include "game/gameplay_config.hpp"
#include <exception>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace {
auto readFile(const std::string& path) -> std::string {
std::ifstream file(path);
if (!file) { return {}; }
std::stringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
// Llig una subclau booleana obligatòria (`section.key`). Retorna
// false si la secció o la clau no hi són o el tipus no és bool —
// qualsevol d'aquests casos és una irregularitat que dispara
// reescriptura del fitxer.
auto readRequiredBool(const fkyaml::node& root, const char* section, const char* key, bool& out)
-> bool {
if (!root.contains(section)) { return false; }
const auto& sec = root[section];
if (!sec.contains(key)) { return false; }
try {
out = sec[key].get_value<bool>();
return true;
} catch (const std::exception&) {
return false;
}
}
// Escriu el fitxer amb els valors actuals + comentaris d'ajuda.
// Format estable perquè l'usuari pot editar-lo a mà.
void writeFile(const std::string& path,
int version,
bool whip_pierces,
bool friendly_fire,
bool players_collide) {
std::ofstream file(path);
if (!file) {
Log::error("gameplay.yaml", "no s'ha pogut escriure {}", path);
return;
}
file << "# gameplay.yaml — tweaks de jugabilitat persistents.\n";
file << "# Edita des del menú de servei (F9 → Tweaks) o a mà.\n";
file << "# Si esborres el fitxer (o aquesta capçalera de versió), es regenerarà\n";
file << "# amb els defaults a la propera arrencada.\n";
file << "\n";
file << "version: " << version << "\n";
file << "\n";
file << "whip:\n";
file << " damage_pierces_walls: " << (whip_pierces ? "true" : "false") << "\n";
file << "\n";
file << "combat:\n";
file << " friendly_fire: " << (friendly_fire ? "true" : "false") << "\n";
file << "\n";
file << "players:\n";
file << " collide_with_each_other: " << (players_collide ? "true" : "false") << "\n";
}
} // namespace
auto GameplayConfig::get() -> GameplayConfig& {
static GameplayConfig instance_;
return instance_;
}
void GameplayConfig::load(const std::string& file_path) {
file_path_ = file_path;
std::error_code error;
const bool EXISTS = std::filesystem::exists(file_path, error);
const auto CONTENT = EXISTS ? readFile(file_path) : std::string{};
// Tots els valors van a temporaris; només ens els quedem si la
// validació completa passa. Així una validació parcial (p. ex. dues
// claus bones i una de dolenta) no deixa l'estat a mig camí.
bool whip_pierces = whip_damage_pierces_walls_;
bool ff = friendly_fire_;
bool players_collide = players_collide_;
auto rewrite_with_defaults = [&](const char* reason) {
// Resetem els camps als defaults compilats abans de reescriure.
// Necessari si load() es crida diverses vegades amb fitxers
// distints; el primer pot haver mutat l'estat.
whip_damage_pierces_walls_ = true;
friendly_fire_ = true;
players_collide_ = true;
writeFile(file_path,
CURRENT_VERSION,
whip_damage_pierces_walls_,
friendly_fire_,
players_collide_);
Log::info("gameplay.yaml", "{} — reescrit a {} amb defaults", reason, file_path);
};
if (!EXISTS) {
rewrite_with_defaults("fitxer absent");
return;
}
if (CONTENT.empty()) {
rewrite_with_defaults("fitxer buit");
return;
}
fkyaml::node yaml;
try {
yaml = fkyaml::node::deserialize(CONTENT);
} catch (const std::exception& e) {
rewrite_with_defaults((std::string("parse error: ") + e.what()).c_str());
return;
}
// `version` ha de ser un enter present i igual a CURRENT_VERSION.
// Qualsevol altra cosa (absent, tipus no enter, valor diferent) és
// una irregularitat: el fitxer ve d'una versió incompatible o ha
// estat manipulat de manera inesperada.
if (!yaml.contains("version")) {
rewrite_with_defaults("version absent");
return;
}
int version = 0;
try {
version = yaml["version"].get_value<int>();
} catch (const std::exception&) {
rewrite_with_defaults("version no és enter");
return;
}
if (version != CURRENT_VERSION) {
rewrite_with_defaults("version desconeguda");
return;
}
if (!readRequiredBool(yaml, "whip", "damage_pierces_walls", whip_pierces) || !readRequiredBool(yaml, "combat", "friendly_fire", ff) || !readRequiredBool(yaml, "players", "collide_with_each_other", players_collide)) {
rewrite_with_defaults("clau requerida absent o de tipus incorrecte");
return;
}
// Tot validat: ens quedem els valors llegits.
whip_damage_pierces_walls_ = whip_pierces;
friendly_fire_ = ff;
players_collide_ = players_collide;
}
void GameplayConfig::save() const {
if (file_path_.empty()) {
// Sense path establert: cap consumidor s'ha registrat encara.
// Pot passar en tests que no carreguen el fitxer; silenciem el cas.
return;
}
writeFile(file_path_,
CURRENT_VERSION,
whip_damage_pierces_walls_,
friendly_fire_,
players_collide_);
}
+71
View File
@@ -0,0 +1,71 @@
#pragma once
#include <string>
// Tweaks de jugabilitat persistents per usuari. Carregat de
// `gameplay.yaml` dins la carpeta de sistema (vegeu
// `Director::createSystemFolder` per la ubicació real per plataforma).
//
// Diferent d'UiConfig (data/config/ui.yaml, només lectura): aquest fitxer
// és escrivible i el ServiceMenu en modifica els valors. Els defaults
// compilats són els que veuen els nous usuaris fins que toquen el menú.
//
// Política d'integritat: davant qualsevol irregularitat (fitxer absent,
// parse error, `version` absent o diferent, clau requerida absent o de
// tipus incorrecte), `load()` reseteja a defaults i reescriu el fitxer.
// No es preserven fitxers parcialment vàlids — així evitem dragar
// versions antigues incoherents quan l'esquema evolucioni.
class GameplayConfig {
public:
// Esquema actual del fitxer. Incrementar quan canviï l'estructura
// (renomenar/treure claus o canviar semàntica). Els fitxers d'usuari
// amb una `version` distinta es reescriuen amb els defaults nous.
// Afegir noves claus opcionals no requereix bump si els defaults
// compilats són equivalents al comportament anterior.
static constexpr int CURRENT_VERSION = 1;
static auto get() -> GameplayConfig&;
GameplayConfig(const GameplayConfig&) = delete;
GameplayConfig(GameplayConfig&&) = delete;
auto operator=(const GameplayConfig&) -> GameplayConfig& = delete;
auto operator=(GameplayConfig&&) -> GameplayConfig& = delete;
// Llig el fitxer i guarda la ruta per a `save()`. Si no existeix,
// l'escriu amb els defaults compilats. Idempotent: pot cridar-se
// múltiples vegades (l'última crida fixa el path actiu).
void load(const std::string& file_path);
// Persisteix l'estat actual al path passat a `load()`. Cridat pel
// ServiceMenu a cada canvi de toggle. No-op si `load()` no s'ha
// cridat encara (sense path no podem escriure enlloc útil).
void save() const;
// Tweaks. Defaults: tots true.
//
// - whip_damage_pierces_walls: el dany del látigo travessa sarcòfags
// (NO el mur extern del playfield, que retalla igualment per
// evitar que el rect surti de la sala visible).
// - friendly_fire: un latigazo pot impactar l'altre jugador.
// - players_collide: els jugadors col·lisionen físicament entre si
// en lloc de travessar-se.
[[nodiscard]] auto whipDamagePiercesWalls() const -> bool { return whip_damage_pierces_walls_; }
[[nodiscard]] auto friendlyFire() const -> bool { return friendly_fire_; }
[[nodiscard]] auto playersCollide() const -> bool { return players_collide_; }
void setWhipDamagePiercesWalls(bool v) { whip_damage_pierces_walls_ = v; }
void setFriendlyFire(bool v) { friendly_fire_ = v; }
void setPlayersCollide(bool v) { players_collide_ = v; }
private:
GameplayConfig() = default;
~GameplayConfig() = default;
// Ruta absoluta al fitxer persistit. Buida fins que algú crida
// `load()` per primera vegada; mentre estigui buida, `save()` és no-op.
std::string file_path_;
bool whip_damage_pierces_walls_{true};
bool friendly_fire_{true};
bool players_collide_{true};
};
+7 -13
View File
@@ -1,13 +1,13 @@
#include "game/levels/level_manager.hpp"
#include <algorithm>
#include <iostream>
#include <stdexcept>
#include <utility>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/defaults.hpp"
#include "utils/log.hpp"
namespace Levels {
@@ -46,8 +46,7 @@ namespace Levels {
throw std::runtime_error("LevelManager: la seqüència de " + PATH + " és buida");
}
index_ = 0;
std::cout << "LevelManager: " << entries_.size() << " sales carregades de "
<< PATH << " (començant a " << currentLabel() << ")\n";
Log::info("LevelManager", "{} sales carregades de {} (començant a {})", entries_.size(), PATH, currentLabel());
}
auto LevelManager::currentPath() const -> const std::string& {
@@ -67,22 +66,20 @@ namespace Levels {
auto LevelManager::advance() -> bool {
if (index_ + 1 >= count()) { return false; }
++index_;
std::cout << "LevelManager: avança a " << currentLabel()
<< " (" << currentPath() << ")\n";
Log::info("LevelManager", "avança a {} ({})", currentLabel(), currentPath());
return true;
}
void LevelManager::restart() {
index_ = 0;
std::cout << "LevelManager: restart a " << currentLabel() << '\n';
Log::info("LevelManager", "restart a {}", currentLabel());
}
void LevelManager::setIndex(int new_index) {
const int CLAMPED = std::clamp(new_index, 0, count() - 1);
if (CLAMPED != index_) {
index_ = CLAMPED;
std::cout << "LevelManager: setIndex(" << new_index << ") → "
<< currentLabel() << " (" << currentPath() << ")\n";
Log::info("LevelManager", "setIndex({}) → {} ({})", new_index, currentLabel(), currentPath());
}
}
@@ -90,14 +87,11 @@ namespace Levels {
for (std::size_t i = 0; i < entries_.size(); ++i) {
if (entries_[i].pyramid == pyramid && entries_[i].room == room) {
index_ = static_cast<int>(i);
std::cout << "LevelManager: starting_point P" << pyramid << "-R" << room
<< " trobat a l'índex " << i << " (" << currentPath() << ")\n";
Log::info("LevelManager", "starting_point P{}-R{} trobat a l'índex {} ({})", pyramid, room, i, currentPath());
return true;
}
}
std::cerr << "LevelManager: starting_point P" << pyramid << "-R" << room
<< " no trobat a levels.yaml — es manté l'índex actual ("
<< currentLabel() << ")\n";
Log::warn("LevelManager", "starting_point P{}-R{} no trobat a levels.yaml — es manté l'índex actual ({})", pyramid, room, currentLabel());
return false;
}
+2 -6
View File
@@ -1,11 +1,11 @@
#include "game/map/map.hpp"
#include <cmath>
#include <iostream>
#include <stdexcept>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace {
@@ -90,11 +90,7 @@ namespace Map {
// canviarà el flag a true quan transiti a OPEN.
door_open = false;
std::cout << "Map: geometria " << grid_cols << "x" << grid_rows
<< " sarcs " << sarc_w << "x" << sarc_h
<< " pitch " << pitch_x << "x" << pitch_y
<< " door_col " << door_col
<< " des de " << yaml_path << '\n';
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);
}
auto getSarcRect(int col, int row) -> SDL_FRect {
+2 -4
View File
@@ -3,7 +3,6 @@
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <variant>
#include "core/rendering/screen.hpp"
@@ -11,6 +10,7 @@
#include "core/resources/resource_cache.hpp"
#include "game/map/map.hpp"
#include "game/map/tilemap.hpp"
#include "utils/log.hpp"
// CONTRACTE BUS DE PINTAT
// -----------------------
@@ -89,9 +89,7 @@ PaintLayer::PaintLayer(const Tilemap& tilemap, EventBus* bus)
});
}
std::cout << "PaintLayer: " << cols_ << "x" << rows_ << " tiles ("
<< width_px_ << "x" << height_px_ << " px), tileset "
<< tileset_path_ << '\n';
Log::info("PaintLayer", "{}x{} tiles ({}x{} px), tileset {}", cols_, rows_, width_px_, height_px_, tileset_path_);
}
PaintLayer::~PaintLayer() {
+2 -3
View File
@@ -3,7 +3,6 @@
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <vector>
@@ -12,6 +11,7 @@
#include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
#ifdef _DEBUG
#include "game/map/tilemap_writer.hpp"
@@ -97,8 +97,7 @@ Tilemap::Tilemap(const std::string& yaml_path) {
}
}
std::cout << "Tilemap: " << cols_ << "x" << rows_ << " tiles ("
<< width_px_ << "x" << height_px_ << " px) des de " << yaml_path << '\n';
Log::info("Tilemap", "{}x{} tiles ({}x{} px) des de {}", cols_, rows_, width_px_, height_px_, yaml_path);
}
#ifdef _DEBUG
+14 -15
View File
@@ -3,7 +3,6 @@
#include <algorithm>
#include <array>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <string_view>
@@ -12,6 +11,7 @@
#include "core/input/gamepad_manager.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/defines.hpp"
#include "utils/log.hpp"
namespace {
@@ -104,7 +104,7 @@ namespace {
std::ofstream dst(path + ".bak", std::ios::binary);
if (!dst) { return; }
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) {
@@ -463,7 +463,7 @@ namespace {
try {
auto yaml = fkyaml::node::deserialize(CONTENT);
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);
presets = defaults_fn();
return save_fn();
@@ -475,15 +475,15 @@ namespace {
presets.push_back(preset);
}
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);
presets = defaults_fn();
return save_fn();
}
return true;
} catch (const std::exception& e) {
std::cout << "Options: error parsejant " << kind_label << ": " << e.what() << '\n';
std::cout << "Options: reescrivint el fitxer amb presets per defecte.\n";
Log::error("Options", "error parsejant {}: {}", kind_label, e.what());
Log::info("Options", "reescrivint el fitxer amb presets per defecte.");
backupFile(path);
presets = defaults_fn();
return save_fn();
@@ -655,8 +655,7 @@ namespace Options {
// amb els defaults de la versió nova. El fitxer original queda
// a `<path>.bak` per si l'usuari volia preservar tuning manual.
if (file_version != Texts::VERSION) {
std::cout << "Options: config.yaml versió \"" << file_version
<< "\" != \"" << Texts::VERSION << "\"; reescrivint amb defaults.\n";
Log::warn("Options", "config.yaml versió \"{}\" != \"{}\"; reescrivint amb defaults.", file_version, Texts::VERSION);
backupFile(g_config_file_path);
return saveToFile();
}
@@ -679,8 +678,8 @@ namespace Options {
if (CHANGED) { saveToFile(); }
return true;
} catch (const std::exception& e) {
std::cout << "Options: error parsejant config.yaml: " << e.what() << '\n';
std::cout << "Options: reescrivint el fitxer amb defaults.\n";
Log::error("Options", "error parsejant config.yaml: {}", e.what());
Log::info("Options", "reescrivint el fitxer amb defaults.");
backupFile(g_config_file_path);
return saveToFile();
}
@@ -692,7 +691,7 @@ namespace Options {
g_config_version = Texts::VERSION;
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 << "locale: \"" << g_locale << "\"\n\n";
@@ -780,7 +779,7 @@ namespace Options {
g_postfx_file_path,
g_postfx_presets,
&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.");
}
@@ -803,7 +802,7 @@ namespace Options {
g_crtpi_file_path,
g_crtpi_presets,
&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.");
}
@@ -843,7 +842,7 @@ namespace Options {
const auto IT = std::ranges::find_if(g_crtpi_presets,
[](const CrtPiPreset& preset) { return preset.name == g_video.shader.current_crtpi_preset; });
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 {
@@ -871,7 +870,7 @@ namespace Options {
const auto IT = std::ranges::find_if(g_postfx_presets,
[](const PostFXPreset& preset) { return preset.name == g_video.shader.current_postfx_preset; });
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 {
+167
View File
@@ -0,0 +1,167 @@
#include "game/scenes/game_over_fsm.hpp"
#include <algorithm>
#include <array>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
#include "utils/log.hpp"
namespace {
// Llegeix un sub-camp sencer d'un node YAML. Si manca, retorna el
// fallback. Mateix patró que `RoomFsm::loadConfig`.
auto readChildInt(const fkyaml::node& parent, const char* key, int fallback) -> int {
if (!parent.contains(key)) { return fallback; }
return parent[key].get_value<int>();
}
} // namespace
namespace GameOverFsm {
auto loadConfig(const std::string& yaml_path) -> Config {
Config cfg{};
auto bytes = Resource::Helper::loadFile(yaml_path);
if (bytes.empty()) {
Log::warn("GameOverFsm", "{} absent — uso fallbacks compilats", yaml_path);
return cfg;
}
const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
if (yaml.contains("game_over")) {
const auto& n = yaml["game_over"];
cfg.continue_text_enter_ms = readChildInt(n, "continue_text_enter_ms", cfg.continue_text_enter_ms);
cfg.continue_text_hold_ms = readChildInt(n, "continue_text_hold_ms", cfg.continue_text_hold_ms);
cfg.continue_text_exit_ms = readChildInt(n, "continue_text_exit_ms", cfg.continue_text_exit_ms);
cfg.continue_count_start = readChildInt(n, "continue_count_start", cfg.continue_count_start);
cfg.game_over_text_hold_ms = readChildInt(n, "game_over_text_hold_ms", cfg.game_over_text_hold_ms);
cfg.game_over_text_exit_ms = readChildInt(n, "game_over_text_exit_ms", cfg.game_over_text_exit_ms);
cfg.game_over_wait_ms = readChildInt(n, "game_over_wait_ms", cfg.game_over_wait_ms);
cfg.fade_out_ms = readChildInt(n, "fade_out_ms", cfg.fade_out_ms);
cfg.black_hold_ms = readChildInt(n, "black_hold_ms", cfg.black_hold_ms);
if (n.contains("continue_music_speed")) {
cfg.continue_music_speed = n["continue_music_speed"].get_value<float>();
}
if (n.contains("game_over_music_track")) {
cfg.game_over_music_track = n["game_over_music_track"].get_value<std::string>();
}
if (n.contains("countdown_sound_key")) {
cfg.countdown_sound_key = n["countdown_sound_key"].get_value<std::string>();
}
cfg.skip_below_or_equal = readChildInt(n, "skip_below_or_equal", cfg.skip_below_or_equal);
}
Log::info("GameOverFsm", "timings carregats des de {} (continue hold {}ms, fade {}ms)", yaml_path, cfg.continue_text_hold_ms, cfg.fade_out_ms);
return cfg;
}
auto advance(State current, Transition& t, const Config& cfg, int delta_ms) -> State {
if (current == State::OFF || current == State::DONE) { return current; }
t.elapsed_ms += delta_ms;
if (current == State::CONTINUE_PROMPT) {
const std::array<int, 3> DURS = {
cfg.continue_text_enter_ms,
cfg.continue_text_hold_ms,
cfg.continue_text_exit_ms,
};
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
t.elapsed_ms -= DURS[t.phase];
// Si acabem la fase 1 (hold) sense cancel·lació, saltem
// directament a GAME_OVER — el panel queda amunt i no
// es reprodueix l'exit anim. La fase 2 (exit) només
// s'executa si el GameScene l'ha forçada amb
// `cancelContinuePrompt`.
if (t.phase == 1) {
return State::GAME_OVER;
}
++t.phase;
}
if (t.phase >= static_cast<int>(DURS.size())) { return State::OFF; }
return current;
}
if (current == State::GAME_OVER) {
const std::array<int, 3> DURS = {
cfg.game_over_text_hold_ms,
cfg.game_over_text_exit_ms,
cfg.game_over_wait_ms,
};
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
t.elapsed_ms -= DURS[t.phase];
++t.phase;
}
if (t.phase >= static_cast<int>(DURS.size())) { return State::FADE_OUT; }
return current;
}
// FADE_OUT
const std::array<int, 2> DURS = {
cfg.fade_out_ms,
cfg.black_hold_ms,
};
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
t.elapsed_ms -= DURS[t.phase];
++t.phase;
}
if (t.phase >= static_cast<int>(DURS.size())) { return State::DONE; }
return current;
}
void cancelContinuePrompt(State current, Transition& t) {
if (current != State::CONTINUE_PROMPT) { return; }
if (t.phase >= 2) { return; } // ja en exit anim, no-op
t.phase = 2;
t.elapsed_ms = 0;
}
void skipToNextCountdown(State current, Transition& t, const Config& cfg) {
if (current != State::CONTINUE_PROMPT) { return; }
if (t.phase != 1) { return; }
const int START = std::max(0, cfg.continue_count_start);
const int STEP_COUNT = START + 1;
const int STEP_MS = std::max(1, cfg.continue_text_hold_ms / STEP_COUNT);
const int CONSUMED = t.elapsed_ms / STEP_MS;
const int CURRENT = std::clamp(START - CONSUMED, 0, START);
// No saltem si ja som al 0: el següent "número" és la transició
// a GAME_OVER, que `advance` ja gestiona quan el hold s'esgota.
if (CURRENT <= 0) { return; }
t.elapsed_ms = (CONSUMED + 1) * STEP_MS;
}
auto countdownSecondsRemaining(State current, const Transition& t, const Config& cfg) -> int {
if (current != State::CONTINUE_PROMPT) { return 0; }
if (t.phase != 1) { return 0; }
// La sèrie és [start, start-1, ..., 1, 0]: en total `start+1`
// graons. El primer número visible és `start` i l'últim 0 es
// manté durant el seu graó abans que la fase 1 acabi i la FSM
// salti a GAME_OVER. Si `start == 0` només es mostra "0"
// durant tot el hold.
const int START = std::max(0, cfg.continue_count_start);
const int STEP_COUNT = START + 1;
const int STEP_MS = std::max(1, cfg.continue_text_hold_ms / STEP_COUNT);
const int CONSUMED = t.elapsed_ms / STEP_MS;
return std::clamp(START - CONSUMED, 0, START);
}
auto stateName(State s) -> const char* {
switch (s) {
case State::OFF:
return "OFF";
case State::CONTINUE_PROMPT:
return "CONTINUE_PROMPT";
case State::GAME_OVER:
return "GAME_OVER";
case State::FADE_OUT:
return "FADE_OUT";
case State::DONE:
return "DONE";
}
return "?";
}
} // namespace GameOverFsm
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff. Show more