Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa77f4cee3 | ||
|
|
a02706ac8b | ||
|
|
2aef925286 | ||
|
|
8eba52353b | ||
|
|
15a4016407 | ||
|
|
2272d3a37a | ||
|
|
3a42c7958a | ||
|
|
dacc4a9909 | ||
|
|
771ff1f6cd | ||
|
|
24609d6c5c | ||
|
|
9d0dde38f9 | ||
|
|
b76a3eb1f3 | ||
|
|
a779533d61 | ||
|
|
bc6ad6f718 | ||
|
|
9bd5e7db33 | ||
|
|
5cc8e349a5 | ||
|
|
0046d526b0 | ||
|
|
fc9947b87f | ||
|
|
35aaa6c519 | ||
|
|
3195746189 | ||
|
|
d1d5679d14 | ||
|
|
ebb161f1f5 | ||
|
|
68be8e55ec | ||
|
|
5ed963b53d | ||
|
|
5e5ff835a9 | ||
|
|
f5d251c33b | ||
|
|
d186f66c2c | ||
|
|
da361a1452 | ||
|
|
a43ae21014 | ||
|
|
6749780356 | ||
|
|
1c49248b72 | ||
|
|
63a03ae632 | ||
|
|
a8bff31a7c | ||
|
|
793e87a203 | ||
|
|
642e0653b2 | ||
|
|
d26abcb9d5 | ||
|
|
a358d8ddbd | ||
|
|
f7c4d62234 | ||
|
|
06a49e09ba | ||
|
|
629baaaec3 | ||
|
|
932d195eb0 | ||
|
|
3e31a88f78 | ||
|
|
cba46642c6 | ||
|
|
8c84892250 | ||
|
|
d14da0e61f | ||
|
|
8bdc994b75 | ||
|
|
6a70d6de9f | ||
|
|
2523661c8f | ||
|
|
fa27308897 | ||
|
|
e58eb10d6d | ||
|
|
a29ea641ab | ||
|
|
28ff425c89 | ||
|
|
e8418f89e0 | ||
|
|
9ca09484a6 | ||
|
|
9291ef6572 | ||
|
|
6f8ab74d6b | ||
|
|
881e0ea1f6 | ||
|
|
6a6a1a52c7 | ||
|
|
62f4b857ef | ||
|
|
3b378e2b29 | ||
|
|
041447eaa4 | ||
|
|
a6b2b02930 | ||
|
|
b7811180ba | ||
|
|
2cbf1bb6e0 | ||
|
|
8890fa9e1f | ||
|
|
23f7cb03a6 | ||
|
|
486a440668 | ||
|
|
a717411edd | ||
|
|
5e862ceadc | ||
|
|
78e164fce4 | ||
|
|
c13b11c501 | ||
|
|
8921b9fd0c | ||
|
|
1d9fea28fe | ||
|
|
d0d5cb0e66 | ||
|
|
e25ccd02fd | ||
|
|
3635cebf25 | ||
|
|
cfbcdc236b | ||
|
|
7871d3930e | ||
|
|
932b457725 | ||
|
|
e9f99a0542 | ||
|
|
0d58da536c | ||
|
|
583acb9053 | ||
|
|
babb111b6f | ||
|
|
866e133aa9 | ||
|
|
170521aba6 | ||
|
|
c8882a3f6e | ||
|
|
5fce52e0ad | ||
|
|
0b708f1515 | ||
|
|
6e99fd0a8b | ||
|
|
1243b457be | ||
|
|
c910a16834 | ||
|
|
0ec1a2548a | ||
|
|
4ea8ab7b6d | ||
|
|
05465b791e | ||
|
|
9949096fba |
No files matched your search
@@ -8,8 +8,8 @@ dist/
|
||||
*.dylib
|
||||
*.dll
|
||||
*.exe
|
||||
aee_2026
|
||||
_aee_2026
|
||||
aee_arcade
|
||||
_aee_arcade
|
||||
.vscode/
|
||||
.idea/
|
||||
.cache/
|
||||
|
||||
@@ -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)
|
||||
@@ -52,6 +52,7 @@ set(ENTITY_SOURCES_BASE
|
||||
source/game/entities/concrete/floor_trap.cpp
|
||||
source/game/entities/concrete/hole.cpp
|
||||
source/game/entities/concrete/dust.cpp
|
||||
source/game/entities/concrete/solid_props.cpp
|
||||
)
|
||||
|
||||
# Entitats que criden directament símbols de Player (com WhipAttack via
|
||||
@@ -80,6 +81,7 @@ set(APP_SOURCES
|
||||
source/core/input/mouse.cpp
|
||||
source/core/rendering/screen.cpp
|
||||
source/core/rendering/canvas.cpp
|
||||
source/core/rendering/lighting.cpp
|
||||
source/core/rendering/notifier.cpp
|
||||
source/core/rendering/render_info.cpp
|
||||
source/core/rendering/gif.cpp
|
||||
@@ -94,6 +96,7 @@ set(APP_SOURCES
|
||||
source/core/rendering/sprite/animated_sprite_parser.cpp
|
||||
source/core/rendering/effects/heat_haze.cpp
|
||||
source/core/rendering/effects/scan_reveal.cpp
|
||||
source/core/rendering/effects/shadow.cpp
|
||||
source/core/rendering/transitions/mosaic.cpp
|
||||
source/core/rendering/transitions/fade_to_black.cpp
|
||||
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
||||
@@ -104,18 +107,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 +225,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 +370,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 +400,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 +430,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 +470,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,4 +1,4 @@
|
||||
PROJECT := aee_2026
|
||||
PROJECT := aee_arcade
|
||||
BUILDDIR := build
|
||||
|
||||
# Detecció de plataforma. En Windows CMake defaulteja a "NMake Makefiles"
|
||||
@@ -293,8 +293,14 @@ rebuild: clean all
|
||||
# Empaqueta data/ a $(BUILDDIR)/resources.pack (format AE26). Força un rebuild
|
||||
# del pack encara que res no hagi canviat dins data/. L'empaquetat també es fa
|
||||
# automàticament a cada `make`/`make debug` via el target CMake `resource_pack`.
|
||||
#
|
||||
# NO passem `-DCMAKE_BUILD_TYPE`: així respectem el tipus ja configurat al
|
||||
# `CMakeCache.txt` (Debug si vens de `make debug`, Release si vens de
|
||||
# `make release`). Si forcéssim Release ací, el següent build de debug
|
||||
# invalidaria la cache i recompilaria-ho tot. El binari `pack_resources`
|
||||
# és una utilitat petita on el build type és irrellevant.
|
||||
pack:
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) $(CMAKE_DEFS)
|
||||
@cmake --build $(BUILDDIR) --target pack_resources
|
||||
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
||||
|
||||
|
||||
@@ -8,6 +8,6 @@ picker:
|
||||
|
||||
# Colors ARGB en hex. Accepta enter (0xFF2244FF) o cadena ("#FF2244FF").
|
||||
colors:
|
||||
border: "0xFF2244FF" # vora 1 px que envolta canvas i picker
|
||||
picker_bg: "0xFF101824" # fons del picker
|
||||
border: "0xFF6A86C8" # vora 1 px que envolta canvas i picker (blau suau)
|
||||
picker_bg: "0xFFC8C0D8" # fons del picker (lavanda clara)
|
||||
picker_hover: "0xFFFFFFFF" # vora 1 px del tile sota el cursor
|
||||
@@ -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
|
||||
@@ -35,9 +35,6 @@ keys:
|
||||
- id: toggle_render_info
|
||||
code: "F10"
|
||||
desc: "Cicle de l'overlay de debug: apagat / dalt / baix"
|
||||
- id: pause
|
||||
code: "F11"
|
||||
desc: "Pausa el joc (o botó START del mando)"
|
||||
- id: menu_toggle
|
||||
code: "F12"
|
||||
desc: "Obri / tanca el menú de servei"
|
||||
@@ -0,0 +1,56 @@
|
||||
# Configuració global del sistema d'il·luminació.
|
||||
#
|
||||
# Cada llum emissora (player, torxa, mòmia, ...) té un únic paràmetre
|
||||
# propi: el radi en píxels. La forma de la llum dins d'aquest radi
|
||||
# (els tres anells del gradient) viu en aquest fitxer perquè és una
|
||||
# decisió de "feel" comuna a totes les entitats — així pots tocar la
|
||||
# corba aquí sense haver de modificar 10 YAMLs.
|
||||
#
|
||||
# Forma del gradient (centre → vora):
|
||||
# 0..inner_ratio del radi → inner_intensity (zona totalment il·luminada)
|
||||
# inner_ratio..mid_ratio → mid_intensity (anell mig)
|
||||
# mid_ratio..1.0 → outer_intensity (anell exterior)
|
||||
# fora del radi → light_level de la sala
|
||||
#
|
||||
# Hi ha dues corbes paral·leles, una per mode, perquè la mateixa escala
|
||||
# 0..1 vol dir coses molt diferents:
|
||||
# `gradient` → subtractive: 1.0 = "no fosqueix" el píxel
|
||||
# (l'emissor "trenca" una sala fosca).
|
||||
# `gradient_additive` → additive: 1.0 = "afegeix el color sencer"
|
||||
# (satura). Valors molt més baixos donen un
|
||||
# halo cromàtic subtil sobre una sala normal.
|
||||
gradient:
|
||||
inner_ratio: 0.5
|
||||
mid_ratio: 0.85
|
||||
inner_intensity: 1.0
|
||||
mid_intensity: 0.8
|
||||
outer_intensity: 0.6
|
||||
|
||||
# Corba del mode additive. Valors petits a propòsit: només volem un
|
||||
# toc de tint cromàtic prop de l'emissor — fora del nucli, la sala
|
||||
# original ha de quedar pràcticament intacta. Si pugeu massa l'inner,
|
||||
# els colors de l'emissor "inunden" la sala (additiu satura ràpid).
|
||||
gradient_additive:
|
||||
inner_ratio: 0.5
|
||||
mid_ratio: 0.85
|
||||
inner_intensity: 0.15
|
||||
mid_intensity: 0.10
|
||||
outer_intensity: 0.05
|
||||
|
||||
# Modulació del radi quan una entitat declara `flicker: true` al seu
|
||||
# bloc `light:`. El radi efectiu per frame és:
|
||||
# r_eff = r_base + (r_amp1·r_base)·sin(2π·f1·t + φ1) + (r_amp2·r_base)·sin(2π·f2·t + φ2)
|
||||
# on (φ1, φ2) es deriven de la seed de l'emissor per a que cada
|
||||
# antorxa balancegi en una fase diferent (sense estat per emissor).
|
||||
# Les amplituds són **ràtios** sobre el radi base: així el feel del
|
||||
# flicker és independent del radi (una torxa de 24 px oscil·la el
|
||||
# mateix % que una de 40 px). Combinant dues sinusoides amb freqüències
|
||||
# incommensurables s'obté un patró "fluid" que el ull llegeix com a
|
||||
# foc; pujar `noise_*` aporta més inquietud, baixar-ho fa la flama
|
||||
# més serena. Aquests són els defaults globals; cada tipus d'entitat
|
||||
# (o cada spawn / sala) pot sobreescriure'ls al seu `light.flicker_params:`.
|
||||
flicker:
|
||||
wave_amplitude_ratio: 0.05 # 5% del radi (per a radi 40 → ±2 px, feel actual)
|
||||
wave_frequency_hz: 3.0
|
||||
noise_amplitude_ratio: 0.025 # 2.5% (per a radi 40 → ±1 px, feel actual)
|
||||
noise_frequency_hz: 7.0
|
||||
@@ -0,0 +1,128 @@
|
||||
# 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_flash: 63
|
||||
# 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. Comparteix la família del CENTER del HUD
|
||||
# (or sobre marró) perquè "Piràmide N / Sala N" sigui sempre daurat
|
||||
# arreu del joc.
|
||||
room_text: 55 # or brillant (#FFDA00, = hud_center.value)
|
||||
room_text_outline: 10 # marró cremat (#482400, = hud_center.shadow)
|
||||
|
||||
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
|
||||
# Parpadeig dels prompts inactius ("Polsa start..." / "Espera..."). Un
|
||||
# únic temporitzador global al Hud sincronitza P1 i P2 i no es reinicia
|
||||
# quan el text canvia entre CAN_JOIN i WAIT. cycle<=0 o ratio>=1 →
|
||||
# sempre visible (disable trivial).
|
||||
blink_cycle_ms: 1000 # durada del cicle complet
|
||||
blink_visible_ratio: 0.8 # proporció del cicle visible (0..1)
|
||||
|
||||
# Famílies de color del HUD per bloc. Cada bloc té `label` (to apagat
|
||||
# per etiquetes), `value` (to viu per a la xifra) i `shadow` (to fosc
|
||||
# del mateix matís — efecte embossing 1 px a baix-dreta). Si manca un
|
||||
# camp, cau al default compilat a `Defaults::UI`. El `colors.hud_flash`
|
||||
# és comú a totes tres famílies.
|
||||
hud_p1:
|
||||
label: 7 # vermell pur
|
||||
value: 183 # crema-préssec
|
||||
shadow: 2 # vermell fosc
|
||||
hud_center:
|
||||
label: 29 # ambre cremat
|
||||
value: 55 # or brillant
|
||||
shadow: 10 # marró cremat
|
||||
hud_p2:
|
||||
label: 248 # cian pur
|
||||
value: 253 # blau pàl·lid
|
||||
shadow: 64 # blau marí
|
||||
|
||||
# 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
|
||||
|
||||
# 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"
|
||||
|
After Width: | Height: | Size: 805 B |
@@ -0,0 +1,22 @@
|
||||
name: column
|
||||
type: decoration
|
||||
editable: true
|
||||
# Sub-picker de frames a l'editor: 6 variants (atlas 2×3 de sub-sprites 16×32).
|
||||
editable_variants: true
|
||||
sprite:
|
||||
path: column.gif
|
||||
size: [16, 32]
|
||||
# Ancoratge "feet": el tile [col, row] del spawn és la base de la columna.
|
||||
# El sprite de 16×32 sobresurt 16 px cap amunt sense afectar col·lisió ni
|
||||
# z-order (la caixa lògica és el tile inferior).
|
||||
anchor: feet
|
||||
tile_size: [1, 1]
|
||||
collision:
|
||||
size: [16, 16]
|
||||
# Mur infranquejable al salt: el jugador no pot passar pel damunt.
|
||||
height: 999
|
||||
flags:
|
||||
collides: true
|
||||
interactable: false
|
||||
animations:
|
||||
default: { frames: [0], frame_ms: 1000, loop: false }
|
||||
@@ -21,9 +21,9 @@ flags:
|
||||
collides: true
|
||||
interactable: true
|
||||
animations:
|
||||
closed: { frames: [1], fps: 1, loop: true }
|
||||
opening: { frames: [0], fps: 6, loop: false }
|
||||
open: { frames: [0], fps: 1, loop: true }
|
||||
closed: { frames: [1], frame_ms: 1000, loop: true }
|
||||
opening: { frames: [0], frame_ms: 167, loop: false }
|
||||
open: { frames: [0], frame_ms: 1000, loop: true }
|
||||
behaviour:
|
||||
states: [CLOSED, OPENING, OPEN]
|
||||
initial_state: CLOSED
|
||||
|
||||
|
Before Width: | Height: | Size: 306 B After Width: | Height: | Size: 303 B |
@@ -16,7 +16,7 @@ flags:
|
||||
# apropiada via `setVariant` abans del primer tick.
|
||||
# jump: frames 0..6 (7 frames) → reproduït al despegue.
|
||||
# land: frames 7..12 (6 frames) → reproduït a l'aterratge.
|
||||
# fps: 10 = 100 ms per frame.
|
||||
# frame_ms: 67 ≈ 15 fps (1000 / 67 ≈ 14.9 fps).
|
||||
animations:
|
||||
jump: { frames: [0, 1, 2, 3, 4, 5, 6], fps: 15, loop: false }
|
||||
land: { frames: [7, 8, 9, 10, 11, 12], fps: 15, loop: false }
|
||||
jump: { frames: [0, 1, 2, 3, 4, 5, 6], frame_ms: 67, loop: false }
|
||||
land: { frames: [7, 8, 9, 10, 11, 12], frame_ms: 67, loop: false }
|
||||
@@ -13,8 +13,8 @@ flags:
|
||||
collides: false
|
||||
interactable: false
|
||||
animations:
|
||||
inactive: { frames: [0], fps: 1, loop: true }
|
||||
active: { frames: [0], fps: 6, loop: true }
|
||||
inactive: { frames: [0], frame_ms: 1000, loop: true }
|
||||
active: { frames: [0], frame_ms: 167, loop: true }
|
||||
cycle:
|
||||
active_duration: 0.3
|
||||
inactive_duration: 2.0
|
||||
|
||||
@@ -22,6 +22,6 @@ flags:
|
||||
# lectura genèrica de flags.
|
||||
triggers_on_overlap: true
|
||||
animations:
|
||||
idle: { frames: [0], fps: 1, loop: true }
|
||||
idle: { frames: [0], frame_ms: 1000, loop: true }
|
||||
stats:
|
||||
damage: 0
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -32,14 +32,28 @@ flags:
|
||||
# no decideix flip per direcció (només selecciona quina animació mostrar).
|
||||
# La momia no té variants `stand_*` perquè mai no para.
|
||||
animations:
|
||||
walk_south: { frames: [0, 1, 2, 3], fps: 8, loop: true }
|
||||
walk_north: { frames: [4, 5, 6, 7], fps: 8, loop: true }
|
||||
walk_east: { frames: [8, 9, 10, 11], fps: 8, loop: true }
|
||||
walk_west: { frames: [8, 9, 10, 11], fps: 8, loop: true, flip_h: true }
|
||||
walk_south: { frames: [0, 1, 2, 3], frame_ms: 125, loop: true }
|
||||
walk_north: { frames: [4, 5, 6, 7], frame_ms: 125, loop: true }
|
||||
walk_east: { frames: [8, 9, 10, 11], frame_ms: 125, loop: true }
|
||||
walk_west: { frames: [8, 9, 10, 11], frame_ms: 125, loop: true, flip_h: true }
|
||||
# Alçament: els 15 primers frames passen ràpid (125 ms cadascun) i el
|
||||
# darrer aguanta 1 s perquè el jugador pugui veure la mòmia ja alçada
|
||||
# abans que comenci a deambular. `frame_ms` accepta una llista per-frame
|
||||
# que mapeja 1:1 contra `frames`.
|
||||
raise:
|
||||
frames: [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]
|
||||
frame_ms: [125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 125, 1000]
|
||||
loop: false
|
||||
ai:
|
||||
type: corridor_wanderer
|
||||
direction_choice: random
|
||||
initial_direction: east
|
||||
|
||||
# Estat SPAWN: la mòmia s'instancia al pixel_rect_ inicial quan
|
||||
# GameScene crida `spawnPendingActors()` (subfase text-exit de
|
||||
# ROOM_INTRO, fase 4) amb l'animació `raise` activa. La duració del
|
||||
# SPAWN la marca la pròpia animació (no-loop): al `completed()`,
|
||||
# transita a ROAMING i comença a deambular.
|
||||
stats:
|
||||
speed: 30
|
||||
damage: 1
|
||||
@@ -58,3 +72,13 @@ 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
|
||||
|
||||
# Halo tènue al voltant de la mòmia. Radi més curt que el del jugador
|
||||
# perquè el rastre lumínic delati la seva posició sense "il·luminar
|
||||
# la sala" — vol ser una pista visual a la foscor.
|
||||
light:
|
||||
radius: 24
|
||||
flicker: true
|
||||
@@ -30,20 +30,20 @@ animations:
|
||||
stun_east: { frames: [18] }
|
||||
stun_west: { frames: [18], flip_h: true }
|
||||
|
||||
walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
||||
walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
||||
walk_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
||||
walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
||||
walk_south: { frames: [1, 2, 3, 0], frame_ms: 100 }
|
||||
walk_north: { frames: [5, 6, 7, 4], frame_ms: 100 }
|
||||
walk_east: { frames: [9, 10, 11, 8], frame_ms: 100 }
|
||||
walk_west: { frames: [9, 10, 11, 8], frame_ms: 100, flip_h: true }
|
||||
|
||||
hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
||||
hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
||||
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
||||
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
||||
|
||||
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] }
|
||||
@@ -97,13 +97,10 @@ turn:
|
||||
south_to_north: west
|
||||
|
||||
# Salt vertical (px de z, no toca la posició al món).
|
||||
# `shadow_y_offset`: puja la sombra per compensar la franja
|
||||
# transparent sota els peus del sprite.
|
||||
jump:
|
||||
impulse: 200
|
||||
gravity: 600
|
||||
landing_stun_ms: 400
|
||||
shadow_y_offset: 3
|
||||
sound: jump.wav
|
||||
land_sound: land.wav
|
||||
|
||||
@@ -119,7 +116,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,10 +133,18 @@ 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:
|
||||
duration_ms: 3000
|
||||
blink_period_ms: 100
|
||||
palette: immune
|
||||
palette: morcus_wash
|
||||
|
||||
# Llum emesa pel jugador. `radius` és el radi del cercle de llum
|
||||
# en píxels del canvas; el gradient (3 anells: 1.0 / 0.9 / 0.8) viu
|
||||
# a data/config/lighting.yaml i és compartit per tots els emissors.
|
||||
# Omet el bloc per desactivar la llum d'aquest personatge.
|
||||
light:
|
||||
radius: 64
|
||||
flicker: true
|
||||
@@ -0,0 +1,19 @@
|
||||
JASC-PAL
|
||||
0100
|
||||
16
|
||||
0 0 0 255
|
||||
150 133 88 255
|
||||
253 233 185 255
|
||||
237 215 165 255
|
||||
214 193 145 255
|
||||
255 252 208 255
|
||||
252 234 189 255
|
||||
239 222 176 255
|
||||
175 190 128 255
|
||||
173 175 115 255
|
||||
159 159 104 255
|
||||
246 211 157 255
|
||||
229 192 139 255
|
||||
213 177 127 255
|
||||
243 226 184 255
|
||||
0 0 0 255
|
||||
@@ -0,0 +1,19 @@
|
||||
JASC-PAL
|
||||
0100
|
||||
16
|
||||
0 0 0 255
|
||||
102 118 132 255
|
||||
226 238 247 255
|
||||
206 216 222 255
|
||||
179 190 198 255
|
||||
245 255 255 255
|
||||
223 239 253 255
|
||||
208 224 238 255
|
||||
124 190 178 255
|
||||
125 170 162 255
|
||||
109 151 150 255
|
||||
221 210 210 255
|
||||
200 187 189 255
|
||||
181 169 175 255
|
||||
213 229 247 255
|
||||
0 0 0 255
|
||||
@@ -0,0 +1,19 @@
|
||||
JASC-PAL
|
||||
0100
|
||||
16
|
||||
0 0 0 255
|
||||
190 190 190 255
|
||||
255 255 255 255
|
||||
255 248 239 255
|
||||
239 233 226 255
|
||||
255 255 255 255
|
||||
255 255 255 255
|
||||
254 254 254 255
|
||||
184 244 213 255
|
||||
193 229 202 255
|
||||
184 216 198 255
|
||||
255 242 225 255
|
||||
255 228 213 255
|
||||
251 216 207 255
|
||||
255 255 255 255
|
||||
0 0 0 255
|
||||
@@ -30,20 +30,20 @@ animations:
|
||||
stun_east: { frames: [18] }
|
||||
stun_west: { frames: [18], flip_h: true }
|
||||
|
||||
walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
||||
walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
||||
walk_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
||||
walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
||||
walk_south: { frames: [1, 2, 3, 0], frame_ms: 100 }
|
||||
walk_north: { frames: [5, 6, 7, 4], frame_ms: 100 }
|
||||
walk_east: { frames: [9, 10, 11, 8], frame_ms: 100 }
|
||||
walk_west: { frames: [9, 10, 11, 8], frame_ms: 100, flip_h: true }
|
||||
|
||||
hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
||||
hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
||||
hurt_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
||||
hurt_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
||||
|
||||
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] }
|
||||
@@ -97,13 +97,10 @@ turn:
|
||||
south_to_north: west
|
||||
|
||||
# Salt vertical (px de z, no toca la posició al món).
|
||||
# `shadow_y_offset`: puja la sombra per compensar la franja
|
||||
# transparent sota els peus del sprite.
|
||||
jump:
|
||||
impulse: 200
|
||||
gravity: 600
|
||||
landing_stun_ms: 400
|
||||
shadow_y_offset: 3
|
||||
sound: jump.wav
|
||||
land_sound: land.wav
|
||||
|
||||
@@ -119,7 +116,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,10 +133,18 @@ 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:
|
||||
duration_ms: 3000
|
||||
blink_period_ms: 100
|
||||
palette: immune
|
||||
palette: sam_wash
|
||||
|
||||
# Llum emesa pel jugador. `radius` és el radi del cercle de llum
|
||||
# en píxels del canvas; el gradient (3 anells: 1.0 / 0.9 / 0.8) viu
|
||||
# a data/config/lighting.yaml i és compartit per tots els emissors.
|
||||
# Omet el bloc per desactivar la llum d'aquest personatge.
|
||||
light:
|
||||
radius: 64
|
||||
flicker: true
|
||||
@@ -0,0 +1,19 @@
|
||||
JASC-PAL
|
||||
0100
|
||||
16
|
||||
0 0 0 255
|
||||
150 133 88 255
|
||||
202 176 123 255
|
||||
191 165 112 255
|
||||
181 156 103 255
|
||||
217 158 115 255
|
||||
194 142 101 255
|
||||
188 143 98 255
|
||||
214 193 145 255
|
||||
196 176 128 255
|
||||
181 161 115 255
|
||||
246 211 157 255
|
||||
229 192 139 255
|
||||
213 177 127 255
|
||||
243 226 184 255
|
||||
0 0 0 255
|
||||
@@ -0,0 +1,19 @@
|
||||
JASC-PAL
|
||||
0100
|
||||
16
|
||||
0 0 0 255
|
||||
102 118 132 255
|
||||
166 169 171 255
|
||||
152 156 158 255
|
||||
140 144 147 255
|
||||
191 144 160 255
|
||||
161 125 146 255
|
||||
152 127 142 255
|
||||
179 190 198 255
|
||||
157 168 179 255
|
||||
139 151 164 255
|
||||
221 210 210 255
|
||||
200 187 189 255
|
||||
181 169 175 255
|
||||
213 229 247 255
|
||||
0 0 0 255
|
||||
@@ -0,0 +1,19 @@
|
||||
JASC-PAL
|
||||
0100
|
||||
16
|
||||
0 0 0 255
|
||||
190 190 190 255
|
||||
235 220 205 255
|
||||
226 212 197 255
|
||||
219 205 191 255
|
||||
255 194 197 255
|
||||
248 184 193 255
|
||||
238 188 189 255
|
||||
239 233 226 255
|
||||
226 220 216 255
|
||||
214 210 208 255
|
||||
255 242 225 255
|
||||
255 228 213 255
|
||||
251 216 207 255
|
||||
255 255 255 255
|
||||
0 0 0 255
|
||||
@@ -13,7 +13,7 @@ flags:
|
||||
collides: true
|
||||
interactable: false
|
||||
animations:
|
||||
roll: { frames: [0], fps: 10, loop: true }
|
||||
roll: { frames: [0], frame_ms: 100, loop: true }
|
||||
ai:
|
||||
type: straight_line
|
||||
initial_direction: right
|
||||
|
||||
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 1.6 KiB |
@@ -19,12 +19,30 @@ flags:
|
||||
collides: true
|
||||
interactable: true
|
||||
animations:
|
||||
closed: { frames: [0], fps: 1, loop: true }
|
||||
opening: { frames: [0], fps: 6, loop: false }
|
||||
open: { frames: [0], fps: 1, loop: true }
|
||||
closed: { frames: [0], frame_ms: 1000, loop: true }
|
||||
opening: { frames: [1, 2], frame_ms: 167, loop: false }
|
||||
open: { frames: [3], frame_ms: 1000, loop: true }
|
||||
behaviour:
|
||||
states: [CLOSED, OPENING, OPEN]
|
||||
initial_state: CLOSED
|
||||
on_perimeter_complete: OPENING
|
||||
on_opening_end: OPEN
|
||||
emits_event_on_open: sarcophagus_opened
|
||||
# Sons opcionals. `opening` es dispara un sol cop en la transició
|
||||
# CLOSED → OPENING (quan el jugador completa el perímetre). Si la clau
|
||||
# manca, no sona res. El basename val tant de clau al Resource::Cache
|
||||
# com de path (`Defaults::Paths::AUDIO_SOUNDS_DIR + key`).
|
||||
#
|
||||
# `opening_pitch_spread`: variació uniforme del ratio de reproducció
|
||||
# centrada a 1.0. Amb 0.05 cada obertura sona entre 0.95× i 1.05× la
|
||||
# velocitat normal (puja/baixa també el to, efecte arcade clàssic).
|
||||
# 0 ⇒ sempre 1.0 (sense variació).
|
||||
sound:
|
||||
opening: open_sarc.wav
|
||||
opening_pitch_spread: 0.5
|
||||
# Recompensa que rep el jugador que pinta el tile que tanca el perímetre
|
||||
# (el GameScene la reparteix consumint `sarcophagus_opened`). Si es lleva
|
||||
# aquest bloc o un dels camps, el camp absent val 0 i no s'aplica.
|
||||
reward:
|
||||
points: 100
|
||||
treasures: 1
|
||||
|
After Width: | Height: | Size: 98 B |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,22 @@
|
||||
name: stone
|
||||
type: decoration
|
||||
editable: true
|
||||
# Sub-picker de frames a l'editor: ofereix una graella amb les 24 variants
|
||||
# del atlas (6×4 sub-sprites 16×16) per escollir quina representa cada
|
||||
# instància col·locada.
|
||||
editable_variants: true
|
||||
sprite:
|
||||
path: stone.gif
|
||||
size: [16, 16]
|
||||
tile_size: [1, 1]
|
||||
collision:
|
||||
size: [16, 16]
|
||||
# Mur infranquejable al salt: el jugador no la pot saltar per damunt.
|
||||
height: 999
|
||||
flags:
|
||||
collides: true
|
||||
interactable: false
|
||||
# Animació "default" amb un sol frame: el spawn YAML pot sobreescriure-la
|
||||
# via `frame: N` per fixar la variant escollida al editor.
|
||||
animations:
|
||||
default: { frames: [0], frame_ms: 1000, loop: false }
|
||||
@@ -15,4 +15,19 @@ flags:
|
||||
# El torch.gif és un strip vertical 16x64 amb 4 frames apilats.
|
||||
# El render clipa cada frame per `anim.currentFrame()` × 16 píxels en Y.
|
||||
animations:
|
||||
idle: { frames: [0, 1, 2, 3], fps: 8, loop: true }
|
||||
idle: { frames: [0, 1, 2, 3], frame_ms: 125, loop: true }
|
||||
|
||||
# Cercle de llum centrat a l'antorxa. El radi és en píxels del canvas;
|
||||
# el gradient global (data/config/lighting.yaml) hi posa la forma (3
|
||||
# anells). Amb `flicker: true` el radi efectiu oscil·la per frame
|
||||
# (suma de dues sinusoides amb fase derivada de la posició) per a
|
||||
# que el cercle "respiri" com una flama; els paràmetres del flicker
|
||||
# també viuen al config global. `offset: [x, y]` desplaça el centre
|
||||
# del cercle: la flama del sprite no està al centre del bounding box
|
||||
# sinó cap a dalt, així que pujem la llum 4 px (Y creix cap avall)
|
||||
# per a que el halo "neixi" de la flama, no del peu. Omet el bloc
|
||||
# per fer una antorxa "apagada".
|
||||
light:
|
||||
radius: 40
|
||||
flicker: true
|
||||
offset: [0, -4]
|
||||
@@ -13,8 +13,8 @@ flags:
|
||||
collides: false
|
||||
interactable: false
|
||||
animations:
|
||||
inactive: { frames: [0], fps: 1, loop: true }
|
||||
active: { frames: [0], fps: 6, loop: true }
|
||||
inactive: { frames: [0], frame_ms: 1000, loop: true }
|
||||
active: { frames: [0], frame_ms: 167, loop: true }
|
||||
cycle:
|
||||
active_duration: 0.5
|
||||
inactive_duration: 1.5
|
||||
|
||||
@@ -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]
|
||||
|
Before Width: | Height: | Size: 313 B After Width: | Height: | Size: 311 B |
|
Before Width: | Height: | Size: 378 B After Width: | Height: | Size: 363 B |
|
Before Width: | Height: | Size: 369 B After Width: | Height: | Size: 348 B |
|
Before Width: | Height: | Size: 313 B After Width: | Height: | Size: 327 B |
@@ -2,7 +2,7 @@
|
||||
# Generado con font_gen.py
|
||||
|
||||
box_width 12
|
||||
box_height 14
|
||||
box_height 13
|
||||
columns 15
|
||||
|
||||
# codepoint_decimal ancho_visual
|
||||
@@ -105,8 +105,8 @@ columns 15
|
||||
193 8 # Á
|
||||
200 7 # È
|
||||
201 7 # É
|
||||
204 4 # Ì
|
||||
205 4 # Í
|
||||
204 3 # Ì
|
||||
205 3 # Í
|
||||
210 9 # Ò
|
||||
211 9 # Ó
|
||||
217 7 # Ù
|
||||
@@ -117,8 +117,8 @@ columns 15
|
||||
225 7 # á
|
||||
232 7 # è
|
||||
233 7 # é
|
||||
236 4 # ì
|
||||
237 4 # í
|
||||
236 3 # ì
|
||||
237 3 # í
|
||||
242 7 # ò
|
||||
243 7 # ó
|
||||
249 7 # ù
|
||||
@@ -127,7 +127,7 @@ columns 15
|
||||
231 7 # ç
|
||||
161 3 # ¡
|
||||
191 7 # ¿
|
||||
171 10 # «
|
||||
187 10 # »
|
||||
171 7 # «
|
||||
187 7 # »
|
||||
183 3 # ·
|
||||
169 10 # ©
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -2,7 +2,7 @@
|
||||
# Generado con font_gen.py
|
||||
|
||||
box_width 12
|
||||
box_height 14
|
||||
box_height 13
|
||||
columns 15
|
||||
|
||||
# codepoint_decimal ancho_visual
|
||||
@@ -105,8 +105,8 @@ columns 15
|
||||
193 9 # Á
|
||||
200 8 # È
|
||||
201 8 # É
|
||||
204 4 # Ì
|
||||
205 4 # Í
|
||||
204 3 # Ì
|
||||
205 3 # Í
|
||||
210 10 # Ò
|
||||
211 10 # Ó
|
||||
217 8 # Ù
|
||||
@@ -117,8 +117,8 @@ columns 15
|
||||
225 8 # á
|
||||
232 8 # è
|
||||
233 8 # é
|
||||
236 4 # ì
|
||||
237 4 # í
|
||||
236 3 # ì
|
||||
237 3 # í
|
||||
242 8 # ò
|
||||
243 8 # ó
|
||||
249 8 # ù
|
||||
@@ -127,7 +127,7 @@ columns 15
|
||||
231 8 # ç
|
||||
161 3 # ¡
|
||||
191 8 # ¿
|
||||
171 10 # «
|
||||
187 10 # »
|
||||
171 7 # «
|
||||
187 7 # »
|
||||
183 3 # ·
|
||||
169 10 # ©
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,4 +1,4 @@
|
||||
# AEE 2026 — Català
|
||||
# AEE ARCADE — Català
|
||||
# lang: ca
|
||||
|
||||
notification:
|
||||
@@ -14,7 +14,7 @@ input:
|
||||
shader_off: "Shaders desactivats"
|
||||
vsync_on: "VSync activat"
|
||||
vsync_off: "VSync desactivat"
|
||||
screenshot: "Captura"
|
||||
screenshot: "Imatge {} guardada a {}"
|
||||
shader: "Shader"
|
||||
preset: "Preset"
|
||||
scaling_disabled: "Escalat sense"
|
||||
@@ -23,7 +23,6 @@ input:
|
||||
scaling_overscan: "Escalat overscan"
|
||||
scaling_integer: "Escalat enter"
|
||||
press_again_exit: "Prem una altra volta per eixir"
|
||||
paused: "Pausa"
|
||||
editor_on: "Editor ON"
|
||||
editor_off: "Editor OFF"
|
||||
editor_save_error: "Editor: error guardant el mapa"
|
||||
@@ -38,6 +37,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 +59,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 +74,7 @@ menu:
|
||||
controls: "Controls"
|
||||
keyboard: "Teclat"
|
||||
gamepad: "Comandament"
|
||||
tweaks: "Jugabilitat"
|
||||
system: "Sistema"
|
||||
labels:
|
||||
player_1: "Jugador 1"
|
||||
@@ -73,6 +86,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 +115,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:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# AEE 2026 — English
|
||||
# AEE ARCADE — English
|
||||
# lang: en
|
||||
|
||||
notification:
|
||||
@@ -14,7 +14,7 @@ input:
|
||||
shader_off: "Shaders off"
|
||||
vsync_on: "VSync on"
|
||||
vsync_off: "VSync off"
|
||||
screenshot: "Screenshot"
|
||||
screenshot: "Image {} saved at {}"
|
||||
shader: "Shader"
|
||||
preset: "Preset"
|
||||
scaling_disabled: "Scaling off"
|
||||
@@ -23,7 +23,6 @@ input:
|
||||
scaling_overscan: "Scaling overscan"
|
||||
scaling_integer: "Scaling integer"
|
||||
press_again_exit: "Press again to exit"
|
||||
paused: "Paused"
|
||||
editor_on: "Editor ON"
|
||||
editor_off: "Editor OFF"
|
||||
editor_save_error: "Editor: map save failed"
|
||||
@@ -38,6 +37,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 +58,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 +73,7 @@ menu:
|
||||
controls: "Controls"
|
||||
keyboard: "Keyboard"
|
||||
gamepad: "Gamepad"
|
||||
tweaks: "Gameplay"
|
||||
system: "System"
|
||||
labels:
|
||||
player_1: "Player 1"
|
||||
@@ -72,6 +85,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 +114,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:
|
||||
|
||||
@@ -10,11 +10,38 @@
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 1
|
||||
room: 1
|
||||
room: 2
|
||||
tileset: data/tilesets/pyramid_1.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Banc de proves del mode `additive` d'il·luminació. La sala es veu
|
||||
# "normal" (level 1.0, el GIF intacte fora del cercle de les torxes)
|
||||
# i només les antorxes hi sumen un halo càlid suau. Player i mummies
|
||||
# desactivats explícitament perquè aquí no volem cap halo al voltant
|
||||
# d'actors — el toc cromàtic és només l'antorxa. La corba additiva
|
||||
# (intensitats baixes a propòsit perquè additive satura ràpid) viu a
|
||||
# `gradient_additive:` de data/config/lighting.yaml.
|
||||
lighting:
|
||||
mode: additive
|
||||
level: 1.0
|
||||
emitters:
|
||||
torches:
|
||||
radius: 20
|
||||
color: [255, 170, 60]
|
||||
# Valors default del flicker explicitats per anar-los ajustant
|
||||
# in-situ. Coincideixen amb els del global a data/config/lighting.yaml;
|
||||
# baixar wave_frequency_hz fa el "palpitar" més lent, baixar les
|
||||
# ratios fa l'oscil·lació més subtil. Qualsevol camp es pot
|
||||
# eliminar i caurà al valor del global.
|
||||
flicker_params:
|
||||
wave_amplitude_ratio: 0.04
|
||||
wave_frequency_hz: 3.0
|
||||
noise_amplitude_ratio: 0.025
|
||||
noise_frequency_hz: 3.0
|
||||
players: false
|
||||
mummies: false
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
@@ -82,6 +109,8 @@ data:
|
||||
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
|
||||
# superior esquerre per verificar render d'una entitat decorativa.
|
||||
entities:
|
||||
- type: torch
|
||||
position: [4, 2]
|
||||
- type: torch
|
||||
position: [8, 2]
|
||||
- type: torch
|
||||
@@ -128,8 +157,8 @@ entities:
|
||||
sarc_position: [2, 4]
|
||||
- type: sarcophagus
|
||||
sarc_position: [3, 4]
|
||||
- type: torch
|
||||
position: [4, 2]
|
||||
- type: door
|
||||
- type: mummy
|
||||
position: [10, 11]
|
||||
position: [10, 7]
|
||||
- type: mummy
|
||||
position: [7, 9]
|
||||
@@ -10,11 +10,38 @@
|
||||
# la cel·la buida (transparent), útil per caselles que ocuparà un actor.
|
||||
|
||||
pyramid: 1
|
||||
room: 2
|
||||
room: 1
|
||||
tileset: data/tilesets/pyramid_1.gif
|
||||
tile_size: 16
|
||||
origin: [-8, 0]
|
||||
|
||||
# Il·luminació de la sala. Bloc opcional; si manca, la sala queda
|
||||
# totalment il·luminada (1.0) i tots els tipus d'entitat emeten amb els
|
||||
# valors declarats al seu YAML de tipus (comportament històric).
|
||||
#
|
||||
# level → atenuació base de la sala (0..1). 1.0 = res
|
||||
# d'efecte, 0.0 = negre fora del radi dels emissors.
|
||||
# El gradient dels emissors viu a
|
||||
# data/config/lighting.yaml.
|
||||
# emitters.<tipus> → override per tipus d'entitat. Tres formes:
|
||||
# false → apaga el tipus
|
||||
# true → defaults del tipus
|
||||
# { radius: N, flicker: B } → camps presents
|
||||
# sobreescriuen el
|
||||
# default; els
|
||||
# absents cauen al
|
||||
# default del tipus.
|
||||
# Plural per llegir-se natural ("totes les momies de la sala"). Per a
|
||||
# casos particulars, un spawn pot dur el seu propi `light:` (mateixes
|
||||
# formes) que té prioritat sobre l'override de sala. La forma antiga
|
||||
# (`light_level:` plà a l'arrel) també segueix sent vàlida per
|
||||
# retrocompatibilitat.
|
||||
lighting:
|
||||
level: 0.15
|
||||
emitters:
|
||||
players: true
|
||||
mummies: false
|
||||
|
||||
# Índex de tile amb què cada jugador deixa rastre en trepitjar un sòl
|
||||
# pintable. Un índex per jugador perquè a multijugador local el recorregut
|
||||
# es distingeix a simple vista. Provisional fins que l'art del pintat
|
||||
@@ -82,16 +109,12 @@ data:
|
||||
# d'obertura (262/263). La momia al corredor central. L'antorxa al costat
|
||||
# superior esquerre per verificar render d'una entitat decorativa.
|
||||
entities:
|
||||
- type: torch
|
||||
position: [4, 2]
|
||||
- type: torch
|
||||
position: [8, 2]
|
||||
- type: torch
|
||||
position: [12, 2]
|
||||
- type: torch
|
||||
position: [16, 2]
|
||||
- type: sarcophagus
|
||||
sarc_position: [0, 0]
|
||||
- type: sarcophagus
|
||||
sarc_position: [1, 0]
|
||||
- type: sarcophagus
|
||||
@@ -118,8 +141,6 @@ entities:
|
||||
sarc_position: [0, 3]
|
||||
- type: sarcophagus
|
||||
sarc_position: [1, 3]
|
||||
- type: sarcophagus
|
||||
sarc_position: [2, 3]
|
||||
- type: sarcophagus
|
||||
sarc_position: [3, 3]
|
||||
- type: sarcophagus
|
||||
@@ -130,11 +151,33 @@ entities:
|
||||
sarc_position: [2, 4]
|
||||
- type: sarcophagus
|
||||
sarc_position: [3, 4]
|
||||
- type: torch
|
||||
position: [4, 2]
|
||||
- type: door
|
||||
# Sense `position`: la porta sempre viu al mur trasser. La columna
|
||||
# transitable la dicta `geometry.door_col`; el render i la caixa
|
||||
# de col·lisió es deriven d'aquesta dada al loadFromLevel.
|
||||
- type: column
|
||||
position: [3, 4]
|
||||
frame: 2
|
||||
- type: stone
|
||||
position: [5, 4]
|
||||
frame: 16
|
||||
- type: stone
|
||||
position: [4, 4]
|
||||
frame: 19
|
||||
- type: column
|
||||
position: [13, 10]
|
||||
frame: 4
|
||||
- type: stone
|
||||
position: [12, 10]
|
||||
frame: 13
|
||||
- type: stone
|
||||
position: [11, 10]
|
||||
frame: 23
|
||||
- type: hole
|
||||
position: [5, 9]
|
||||
- type: hole
|
||||
position: [9, 11]
|
||||
- type: stone
|
||||
position: [10, 10]
|
||||
frame: 18
|
||||
- type: mummy
|
||||
position: [10, 7]
|
||||
- type: mummy
|
||||
position: [7, 9]
|
||||
position: [10, 13]
|
||||
@@ -1,35 +0,0 @@
|
||||
JASC-PAL
|
||||
0100
|
||||
32
|
||||
0 0 0
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
@@ -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
|
||||
@@ -0,0 +1,96 @@
|
||||
# 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 >
|
||||
# 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:
|
||||
# Violeta cerimonial: contrasta amb l'or del text (room_text), no
|
||||
# compite amb el blau del bloc P2 del HUD, i deixa la sorra de les
|
||||
# sales respirar al darrere.
|
||||
color: [72, 36, 85]
|
||||
alpha: 160
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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) {
|
||||
@@ -129,6 +136,22 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||
}
|
||||
}
|
||||
|
||||
// Variant amb velocitat (i to) escalats. Apliquem el ratio al canal
|
||||
// just retornat per `playSound`: així el `SDL_AudioStream` recent creat
|
||||
// processa tot el sample amb el ratio des del primer pull del callback.
|
||||
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
|
||||
// la crida al ratio — sense efectes col·laterals.
|
||||
// NOLINTNEXTLINE(readability-make-member-function-const)
|
||||
void Audio::playSound(const std::string& name, Group group, float speed) {
|
||||
if (!sound_enabled_) { return; }
|
||||
auto* sound = AudioResource::getSound(name);
|
||||
if (sound == nullptr) { return; }
|
||||
const int CH = engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
if (CH >= 0 && speed != 1.0F) {
|
||||
engine_->setChannelSpeed(CH, speed);
|
||||
}
|
||||
}
|
||||
|
||||
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
|
||||
// existeix o l'engine retorna -1 (cap de canals d'efecte plé), cau a playSound
|
||||
// sec — l'usuari sent el so encara que la cua no s'apliqui.
|
||||
|
||||
@@ -76,11 +76,22 @@ 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 amb la velocitat (i to) escalats per `speed`:
|
||||
// 1.0 = normal, 0.95 ≈ -5% (més greu i lent), 1.05 ≈ +5% (més
|
||||
// agut i ràpid). Mateixa semàntica que `setMusicSpeed`. Útil per a
|
||||
// variacions subtils que eviten la fatiga d'escoltar el mateix
|
||||
// sample idèntic (p.ex. obertures de sarcòfag, picks d'ítems).
|
||||
void playSound(const std::string& name, Group group, float speed);
|
||||
// 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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -425,6 +430,12 @@ namespace Ja {
|
||||
return channel;
|
||||
}
|
||||
|
||||
void Engine::setChannelSpeed(const int channel, const float ratio) {
|
||||
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return; }
|
||||
if (channels_[channel].stream == nullptr) { return; }
|
||||
SDL_SetAudioStreamFrequencyRatio(channels_[channel].stream, ratio);
|
||||
}
|
||||
|
||||
void Engine::pauseChannel(const int channel) {
|
||||
forEachTargetChannel(channel, [](Channel& ch) {
|
||||
if (ch.state == ChannelState::PLAYING) {
|
||||
|
||||
@@ -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.
|
||||
@@ -184,6 +192,14 @@ namespace Ja {
|
||||
// --- So ---
|
||||
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
|
||||
auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
|
||||
// Ajusta la velocitat de reproducció d'un canal actiu via
|
||||
// `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal. Igual que a
|
||||
// `setMusicSpeed`, puja/baixa el to junt amb la velocitat
|
||||
// (efecte "chipmunk"); per a SFX curts arcade és el que volem.
|
||||
// No-op si el canal no està actiu. Cridar-lo just després de
|
||||
// `playSound`/`playSoundOnChannel` perquè el ratio cobreixi
|
||||
// tota la reproducció.
|
||||
void setChannelSpeed(int channel, float ratio);
|
||||
// Reproducció amb so processat per un efecte. Retorna el canal
|
||||
// assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS).
|
||||
// El sound original només s'usa per consultar el spec/buffer; el
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
#include "core/input/key_config.hpp"
|
||||
@@ -26,7 +27,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;
|
||||
|
||||
@@ -69,15 +70,26 @@ namespace GlobalInputs {
|
||||
notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off");
|
||||
}
|
||||
|
||||
// Volca el canvas ARGB actual a PNG dins de `screenshots/`. Si ha
|
||||
// fallat l'escriptura, el handler no mostra notificació (retorn buit).
|
||||
// Volca el canvas ARGB actual a PNG dins del directori de
|
||||
// captures (configurat pel Director). La notificació informa
|
||||
// del nom i de la ruta absoluta — així l'usuari sap on anar
|
||||
// a buscar-la encara que el binari s'executi des d'un sitio
|
||||
// estrany. Si l'escriptura falla, no es mostra notificació.
|
||||
void onScreenshot() {
|
||||
const auto* buffer = Screen::get().getCanvasArgb();
|
||||
const int W = Screen::get().getCanvasWidth();
|
||||
const int H = Screen::get().getCanvasHeight();
|
||||
const std::string FILENAME = Screenshot::save(buffer, W, H);
|
||||
if (!FILENAME.empty()) {
|
||||
Notification::notify(Locale::get().text("input.screenshot") + ' ' + FILENAME);
|
||||
const auto R = Screenshot::save(buffer, W, H);
|
||||
if (!R.filename.empty()) {
|
||||
// `make_format_args` retorna un store que conté referències
|
||||
// als arguments; cal lligar-lo a una lvalue local perquè
|
||||
// sobrevisqui fins que `vformat` el consumeixi. Passar-lo
|
||||
// directament a `vformat` és UB en alguns compiladors
|
||||
// (DR de C++20 / fix de C++23) i ha provocat un segfault
|
||||
// post-captura en aquest projecte.
|
||||
const std::string TEMPLATE = Locale::get().text("input.screenshot");
|
||||
auto args = std::make_format_args(R.filename, R.folder);
|
||||
Notification::notify(std::vformat(TEMPLATE, args));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +173,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.");
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "core/rendering/effects/shadow.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
|
||||
namespace Effects::Shadow {
|
||||
|
||||
void render(int center_x, int feet_y, float height_factor) {
|
||||
// Lookup únic al primer ús: la Surface viu a Cache durant tota la
|
||||
// sessió, així evitem la cerca lineal a cada frame.
|
||||
static const auto SURFACE = Resource::Cache::get().getSurface(CACHE_KEY);
|
||||
|
||||
const int FRAME_COUNT = SURFACE->getWidth() / FRAME_SIZE;
|
||||
if (FRAME_COUNT <= 0) { return; }
|
||||
|
||||
// Mapping a bandes uniformes: factor 0 → últim (gran), factor 1 →
|
||||
// primer (petit). Per a 3 fotogrames: [0, 1/3) → 2, [1/3, 2/3) → 1,
|
||||
// [2/3, 1] → 0. Generalitza a qualsevol nombre de fotogrames per
|
||||
// si afegim mides intermèdies.
|
||||
const float CLAMPED = std::clamp(height_factor, 0.0F, 1.0F);
|
||||
int frame_index = static_cast<int>((1.0F - CLAMPED) * static_cast<float>(FRAME_COUNT));
|
||||
frame_index = std::clamp(frame_index, 0, FRAME_COUNT - 1);
|
||||
|
||||
SDL_FRect clip{
|
||||
static_cast<float>(frame_index * FRAME_SIZE),
|
||||
0.0F,
|
||||
static_cast<float>(FRAME_SIZE),
|
||||
static_cast<float>(FRAME_SIZE),
|
||||
};
|
||||
|
||||
// Ancoratge: vora inferior del fotograma a `feet_y` — així el
|
||||
// quadrat 16x16 del sprite encaixa exactament al 16x16 inferior
|
||||
// d'una entitat 16x16/16x32 centrada en `center_x`.
|
||||
const int X = center_x - (FRAME_SIZE / 2);
|
||||
const int Y = feet_y - FRAME_SIZE;
|
||||
SURFACE->render(X, Y, &clip);
|
||||
}
|
||||
|
||||
} // namespace Effects::Shadow
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
namespace Effects::Shadow {
|
||||
|
||||
// Mida de cada fotograma del sprite compartit de la sombra.
|
||||
// El sprite és una tira horitzontal de fotogrames de 16x16, ordenats
|
||||
// del més petit (índex 0) al més gran (últim índex).
|
||||
constexpr int FRAME_SIZE = 16;
|
||||
|
||||
// Clau amb què `Resource::Cache` té carregada la Surface compartida.
|
||||
// El precarrega el Director al boot.
|
||||
constexpr const char* CACHE_KEY = "shadow";
|
||||
|
||||
// Pinta la sombra al canvas. `height_factor` selecciona el fotograma:
|
||||
// 0.0 (entitat al terra) → fotograma més gran, 1.0 (pic del salt o
|
||||
// entitat més alta del terra) → fotograma més petit. La tira es
|
||||
// divideix en bandes uniformes; valors fora de [0, 1] es clampen.
|
||||
//
|
||||
// El fotograma s'ancora al quadrat 16x16 inferior d'una entitat de
|
||||
// 16x16/16x32 centrada en `center_x` amb peus a `feet_y`: la vora
|
||||
// inferior del sprite queda exactament a `feet_y`. La posició
|
||||
// visual de l'oval dins del fotograma la decideix l'artista al GIF.
|
||||
void render(int center_x, int feet_y, float height_factor);
|
||||
|
||||
} // namespace Effects::Shadow
|
||||
@@ -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++;
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
#include "core/rendering/lighting.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <numbers>
|
||||
#include <vector>
|
||||
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
#include "utils/log.hpp"
|
||||
|
||||
namespace Lighting {
|
||||
|
||||
namespace {
|
||||
|
||||
// Buffer de luminància compartit entre frames. Es redimensiona
|
||||
// només si canvia la mida del canvas (mai en un run normal: el
|
||||
// canvas del joc és 320×240 fix). El reusem perquè una `apply`
|
||||
// allotjant cada frame seria un cost evitable.
|
||||
auto luminanceBuffer() -> std::vector<std::uint8_t>& {
|
||||
static std::vector<std::uint8_t> buf_;
|
||||
return buf_;
|
||||
}
|
||||
|
||||
// Corba activa del mode subtractive. loadConfig() la reemplaça
|
||||
// si troba el bloc `gradient:` al YAML.
|
||||
auto activeSubtractiveConfig() -> GradientConfig& {
|
||||
static GradientConfig cfg_{
|
||||
.inner_ratio = Defaults::Lighting::INNER_RATIO,
|
||||
.mid_ratio = Defaults::Lighting::MID_RATIO,
|
||||
.inner_intensity = Defaults::Lighting::INNER_INTENSITY,
|
||||
.mid_intensity = Defaults::Lighting::MID_INTENSITY,
|
||||
.outer_intensity = Defaults::Lighting::OUTER_INTENSITY,
|
||||
};
|
||||
return cfg_;
|
||||
}
|
||||
|
||||
// Corba activa del mode additive. Defaults baixos a propòsit:
|
||||
// en additive la intensitat és l'alfa del color afegit, així
|
||||
// que valors petits (0.05..0.15) donen el "halo subtil" que
|
||||
// sol voler-se. loadConfig() la reemplaça si troba el bloc
|
||||
// `gradient_additive:` al YAML.
|
||||
auto activeAdditiveConfig() -> GradientConfig& {
|
||||
static GradientConfig cfg_{
|
||||
.inner_ratio = Defaults::Lighting::INNER_RATIO_ADDITIVE,
|
||||
.mid_ratio = Defaults::Lighting::MID_RATIO_ADDITIVE,
|
||||
.inner_intensity = Defaults::Lighting::INNER_INTENSITY_ADDITIVE,
|
||||
.mid_intensity = Defaults::Lighting::MID_INTENSITY_ADDITIVE,
|
||||
.outer_intensity = Defaults::Lighting::OUTER_INTENSITY_ADDITIVE,
|
||||
};
|
||||
return cfg_;
|
||||
}
|
||||
|
||||
auto activeFlicker() -> FlickerConfig& {
|
||||
static FlickerConfig fc_{
|
||||
.wave_amplitude_ratio = Defaults::Lighting::FLICKER_WAVE_AMPLITUDE_RATIO,
|
||||
.wave_frequency_hz = Defaults::Lighting::FLICKER_WAVE_FREQUENCY_HZ,
|
||||
.noise_amplitude_ratio = Defaults::Lighting::FLICKER_NOISE_AMPLITUDE_RATIO,
|
||||
.noise_frequency_hz = Defaults::Lighting::FLICKER_NOISE_FREQUENCY_HZ,
|
||||
};
|
||||
return fc_;
|
||||
}
|
||||
|
||||
// Modula el radi base d'un emissor amb la suma de dues sinusoides
|
||||
// (`wave` + `noise`) i una fase derivada de `seed`. La seed és un
|
||||
// identificador estable de l'emissor (no canvia amb la posició) —
|
||||
// així el patró no "deriva" quan l'emissor es mou. El factor
|
||||
// ~φ (golden ratio) dispersa les fases per a seeds consecutives.
|
||||
// Les amplituds són ràtios sobre el radi base, multiplicades aquí
|
||||
// → mateix percentatge d'oscil·lació per a qualsevol mida.
|
||||
auto computeFlickerRadius(int base_radius, int seed, float t_s, const FlickerConfig& fc) -> int {
|
||||
constexpr float TWO_PI = 2.0F * std::numbers::pi_v<float>;
|
||||
constexpr float GOLDEN_FRACTION = 0.6180339F;
|
||||
const float PHASE = static_cast<float>(seed) * GOLDEN_FRACTION;
|
||||
const float W1 = std::sin((TWO_PI * fc.wave_frequency_hz * t_s) + PHASE);
|
||||
const float W2 = std::sin((TWO_PI * fc.noise_frequency_hz * t_s) + (PHASE * 2.7F));
|
||||
const auto R_F = static_cast<float>(base_radius);
|
||||
const float DELTA = (fc.wave_amplitude_ratio * R_F * W1) + (fc.noise_amplitude_ratio * R_F * W2);
|
||||
const int R = base_radius + static_cast<int>(std::lround(DELTA));
|
||||
return std::max(1, R);
|
||||
}
|
||||
|
||||
// Quantitza un factor 0..1 a 0..255. `clamp` per defensar-nos de
|
||||
// YAMLs que entreguen valors fora de rang sense forçar la mà.
|
||||
auto toByte(float v) -> std::uint8_t {
|
||||
const float CLAMPED = std::clamp(v, 0.0F, 1.0F);
|
||||
return static_cast<std::uint8_t>(std::lround(CLAMPED * 255.0F));
|
||||
}
|
||||
|
||||
void readField(const fkyaml::node& node, const char* key, float& dst) {
|
||||
if (node.contains(key)) { dst = node[key].get_value<float>(); }
|
||||
}
|
||||
|
||||
// Pre-calcula els 3 quadrats de radi i les intensitats que farem
|
||||
// servir dins del bucle per píxel. Es resol una sola vegada per
|
||||
// emissor i s'evita arrastrar 6 paràmetres a `rasterizeEmitter`.
|
||||
struct EmitterRings {
|
||||
int radius_sq{0};
|
||||
int mid_sq{0};
|
||||
int inner_sq{0};
|
||||
std::uint8_t inner{0};
|
||||
std::uint8_t mid{0};
|
||||
std::uint8_t outer{0};
|
||||
};
|
||||
|
||||
auto computeRings(int radius, const GradientConfig& cfg) -> EmitterRings {
|
||||
const auto R_F = static_cast<float>(radius);
|
||||
const float R_INNER = R_F * cfg.inner_ratio;
|
||||
const float R_MID = R_F * cfg.mid_ratio;
|
||||
return EmitterRings{
|
||||
.radius_sq = radius * radius,
|
||||
.mid_sq = static_cast<int>(R_MID * R_MID),
|
||||
.inner_sq = static_cast<int>(R_INNER * R_INNER),
|
||||
.inner = toByte(cfg.inner_intensity),
|
||||
.mid = toByte(cfg.mid_intensity),
|
||||
.outer = toByte(cfg.outer_intensity),
|
||||
};
|
||||
}
|
||||
|
||||
// Itera la bounding box de l'emissor clipada al canvas i actualitza
|
||||
// la fila de luminància corresponent amb `max(actual, ring)`. Es
|
||||
// crida un cop per emissor, així extreure-la deixa `apply` simple.
|
||||
void rasterizeEmitter(const Emitter& e, const EmitterRings& rings, std::uint8_t* lum, int width, int height) {
|
||||
const int R = e.radius;
|
||||
const int CX = static_cast<int>(e.cx);
|
||||
const int CY = static_cast<int>(e.cy);
|
||||
const int X0 = std::max(0, CX - R);
|
||||
const int Y0 = std::max(0, CY - R);
|
||||
const int X1 = std::min(width - 1, CX + R);
|
||||
const int Y1 = std::min(height - 1, CY + R);
|
||||
for (int y = Y0; y <= Y1; ++y) {
|
||||
const int DY = y - CY;
|
||||
const int DY_SQ = DY * DY;
|
||||
if (DY_SQ > rings.radius_sq) { continue; }
|
||||
std::uint8_t* row = lum + (static_cast<std::ptrdiff_t>(y) * width);
|
||||
for (int x = X0; x <= X1; ++x) {
|
||||
const int DX = x - CX;
|
||||
const int DIST_SQ = (DX * DX) + DY_SQ;
|
||||
if (DIST_SQ > rings.radius_sq) { continue; }
|
||||
std::uint8_t intensity = rings.outer;
|
||||
if (DIST_SQ <= rings.inner_sq) {
|
||||
intensity = rings.inner;
|
||||
} else if (DIST_SQ <= rings.mid_sq) {
|
||||
intensity = rings.mid;
|
||||
}
|
||||
row[x] = std::max(intensity, row[x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pasada final del mode subtractive: pixel = pixel * lum / 255
|
||||
// per canal. El canal alpha es preserva (la resta del pipeline
|
||||
// assumeix opacitat total al canvas; veure Rendering::OPAQUE_ALPHA).
|
||||
void modulateCanvas(std::uint32_t* canvas, const std::uint8_t* lum, int width, int height) {
|
||||
for (int y = 0; y < height; ++y) {
|
||||
std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width);
|
||||
const std::uint8_t* lrow = lum + (static_cast<std::ptrdiff_t>(y) * width);
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const std::uint8_t L = lrow[x];
|
||||
if (L == 255) { continue; } // píxel intacte.
|
||||
const std::uint32_t SRC = px[x];
|
||||
const std::uint8_t SR = (SRC >> 16) & 0xFF;
|
||||
const std::uint8_t SG = (SRC >> 8) & 0xFF;
|
||||
const std::uint8_t SB = SRC & 0xFF;
|
||||
const auto R = static_cast<std::uint8_t>((SR * L) / 255);
|
||||
const auto G = static_cast<std::uint8_t>((SG * L) / 255);
|
||||
const auto B = static_cast<std::uint8_t>((SB * L) / 255);
|
||||
px[x] = (SRC & 0xFF000000U) |
|
||||
(static_cast<std::uint32_t>(R) << 16) |
|
||||
(static_cast<std::uint32_t>(G) << 8) |
|
||||
static_cast<std::uint32_t>(B);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Atenuació ambient del mode additive: aplica `pixel × level` per
|
||||
// canvas abans del bucle d'emissors. Si `level == 255` (sala a
|
||||
// 1.0) salta com a no-op — el cas comú quan només volem el tint
|
||||
// dels emissors sense oscurir la sala.
|
||||
void darkenCanvas(std::uint32_t* canvas, int width, int height, std::uint8_t level) {
|
||||
if (level == 255) { return; }
|
||||
for (int y = 0; y < height; ++y) {
|
||||
std::uint32_t* px = canvas + (static_cast<std::ptrdiff_t>(y) * width);
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const std::uint32_t SRC = px[x];
|
||||
const std::uint8_t SR = (SRC >> 16) & 0xFF;
|
||||
const std::uint8_t SG = (SRC >> 8) & 0xFF;
|
||||
const std::uint8_t SB = SRC & 0xFF;
|
||||
const auto R = static_cast<std::uint8_t>((SR * level) / 255);
|
||||
const auto G = static_cast<std::uint8_t>((SG * level) / 255);
|
||||
const auto B = static_cast<std::uint8_t>((SB * level) / 255);
|
||||
px[x] = (SRC & 0xFF000000U) |
|
||||
(static_cast<std::uint32_t>(R) << 16) |
|
||||
(static_cast<std::uint32_t>(G) << 8) |
|
||||
static_cast<std::uint32_t>(B);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pasada del mode additive per a un emissor: suma `color × intensity`
|
||||
// a cada píxel cobert pel cercle, saturant a 255 per canal. A
|
||||
// diferència del subtractive, NO usa buffer intermig (no cal max())
|
||||
// perquè la suma és commutativa; emissors solapats acumulen
|
||||
// contribucions de manera natural. Si dues llums de colors diferents
|
||||
// es solapen, el resultat tendeix al blanc en la zona de coincidència
|
||||
// (suma física): el solape s'aclareix més que un sol emissor.
|
||||
void rasterizeEmitterAdditive(const Emitter& e, const EmitterRings& rings, std::uint32_t* canvas, int width, int height) {
|
||||
const int R = e.radius;
|
||||
const int CX = static_cast<int>(e.cx);
|
||||
const int CY = static_cast<int>(e.cy);
|
||||
const int X0 = std::max(0, CX - R);
|
||||
const int Y0 = std::max(0, CY - R);
|
||||
const int X1 = std::min(width - 1, CX + R);
|
||||
const int Y1 = std::min(height - 1, CY + R);
|
||||
for (int y = Y0; y <= Y1; ++y) {
|
||||
const int DY = y - CY;
|
||||
const int DY_SQ = DY * DY;
|
||||
if (DY_SQ > rings.radius_sq) { continue; }
|
||||
std::uint32_t* row = canvas + (static_cast<std::ptrdiff_t>(y) * width);
|
||||
for (int x = X0; x <= X1; ++x) {
|
||||
const int DX = x - CX;
|
||||
const int DIST_SQ = (DX * DX) + DY_SQ;
|
||||
if (DIST_SQ > rings.radius_sq) { continue; }
|
||||
std::uint8_t intensity = rings.outer;
|
||||
if (DIST_SQ <= rings.inner_sq) {
|
||||
intensity = rings.inner;
|
||||
} else if (DIST_SQ <= rings.mid_sq) {
|
||||
intensity = rings.mid;
|
||||
}
|
||||
// contrib_channel = emitter_color_channel × intensity / 255
|
||||
// (intensity ja és 0..255 quantitzat a `toByte`).
|
||||
const int CR = (e.color.r * intensity) / 255;
|
||||
const int CG = (e.color.g * intensity) / 255;
|
||||
const int CB = (e.color.b * intensity) / 255;
|
||||
const std::uint32_t SRC = row[x];
|
||||
const int SR = static_cast<int>((SRC >> 16) & 0xFF);
|
||||
const int SG = static_cast<int>((SRC >> 8) & 0xFF);
|
||||
const int SB = static_cast<int>(SRC & 0xFF);
|
||||
const auto NR = static_cast<std::uint32_t>(std::min(255, SR + CR));
|
||||
const auto NG = static_cast<std::uint32_t>(std::min(255, SG + CG));
|
||||
const auto NB = static_cast<std::uint32_t>(std::min(255, SB + CB));
|
||||
row[x] = (SRC & 0xFF000000U) | (NR << 16) | (NG << 8) | NB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto config() -> const GradientConfig& {
|
||||
return activeSubtractiveConfig();
|
||||
}
|
||||
|
||||
auto flickerConfig() -> const FlickerConfig& {
|
||||
return activeFlicker();
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Llegeix els 5 camps de `GradientConfig` d'un node YAML — qualsevol
|
||||
// camp absent conserva el valor compilat dels defaults. Compartit
|
||||
// entre els blocs `gradient:` i `gradient_additive:`.
|
||||
void readGradient(const fkyaml::node& g, GradientConfig& cfg) {
|
||||
readField(g, "inner_ratio", cfg.inner_ratio);
|
||||
readField(g, "mid_ratio", cfg.mid_ratio);
|
||||
readField(g, "inner_intensity", cfg.inner_intensity);
|
||||
readField(g, "mid_intensity", cfg.mid_intensity);
|
||||
readField(g, "outer_intensity", cfg.outer_intensity);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void loadConfig(const std::string& yaml_path) {
|
||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||
if (bytes.empty()) {
|
||||
Log::warn("Lighting", "no s'ha pogut obrir {} — s'usaran els defaults compilats", yaml_path);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
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("gradient")) {
|
||||
readGradient(yaml["gradient"], activeSubtractiveConfig());
|
||||
}
|
||||
if (yaml.contains("gradient_additive")) {
|
||||
readGradient(yaml["gradient_additive"], activeAdditiveConfig());
|
||||
}
|
||||
if (yaml.contains("flicker")) {
|
||||
const auto& f = yaml["flicker"];
|
||||
auto& fc = activeFlicker();
|
||||
readField(f, "wave_amplitude_ratio", fc.wave_amplitude_ratio);
|
||||
readField(f, "wave_frequency_hz", fc.wave_frequency_hz);
|
||||
readField(f, "noise_amplitude_ratio", fc.noise_amplitude_ratio);
|
||||
readField(f, "noise_frequency_hz", fc.noise_frequency_hz);
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
Log::warn("Lighting", "error parsejant {}: {} — s'usaran els defaults compilats", yaml_path, ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Helper: emissor "patxat" amb el radi efectiu (post-flicker) per
|
||||
// a aquest frame. Mantenim l'`Emitter` del caller intacte i la
|
||||
// resta de camps (color, seed, flicker_params, ...) propagats.
|
||||
auto withEffectiveRadius(const Emitter& e, int r_eff) -> Emitter {
|
||||
return Emitter{
|
||||
.cx = e.cx,
|
||||
.cy = e.cy,
|
||||
.radius = r_eff,
|
||||
.seed = e.seed,
|
||||
.flicker = e.flicker,
|
||||
.color = e.color,
|
||||
.flicker_params = e.flicker_params,
|
||||
};
|
||||
}
|
||||
|
||||
// Camí del mode subtractive (històric): omple un buffer de
|
||||
// luminància amb `max(room, emitter_intensity)` i finalment
|
||||
// multiplica el canvas per ell. Color de l'emissor ignorat.
|
||||
void applySubtractive(std::uint32_t* canvas, int width, int height, std::uint8_t room, std::span<const Emitter> emitters) {
|
||||
auto& lum = luminanceBuffer();
|
||||
const std::size_t TOTAL = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
||||
if (lum.size() != TOTAL) { lum.resize(TOTAL); }
|
||||
std::ranges::fill(lum, room);
|
||||
|
||||
const auto& cfg = activeSubtractiveConfig();
|
||||
const float T_S = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
||||
for (const auto& e : emitters) {
|
||||
if (e.radius <= 0) { continue; }
|
||||
const int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, e.flicker_params) : e.radius;
|
||||
const auto RINGS = computeRings(R_EFF, cfg);
|
||||
const Emitter EFF = withEffectiveRadius(e, R_EFF);
|
||||
rasterizeEmitter(EFF, RINGS, lum.data(), width, height);
|
||||
}
|
||||
modulateCanvas(canvas, lum.data(), width, height);
|
||||
}
|
||||
|
||||
// Camí del mode additive: atenua el canvas per `room` (si <255)
|
||||
// i després suma `color × intensity` per emissor directament al
|
||||
// canvas, sense buffer intermig.
|
||||
void applyAdditive(std::uint32_t* canvas, int width, int height, std::uint8_t room, std::span<const Emitter> emitters) {
|
||||
darkenCanvas(canvas, width, height, room);
|
||||
const auto& cfg = activeAdditiveConfig();
|
||||
const float T_S = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
||||
for (const auto& e : emitters) {
|
||||
if (e.radius <= 0) { continue; }
|
||||
const int R_EFF = e.flicker ? computeFlickerRadius(e.radius, e.seed, T_S, e.flicker_params) : e.radius;
|
||||
const auto RINGS = computeRings(R_EFF, cfg);
|
||||
const Emitter EFF = withEffectiveRadius(e, R_EFF);
|
||||
rasterizeEmitterAdditive(EFF, RINGS, canvas, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void apply(std::uint32_t* canvas, int width, int height, Mode mode, float room_light, std::span<const Emitter> emitters) {
|
||||
if (canvas == nullptr || width <= 0 || height <= 0) { return; }
|
||||
|
||||
// Atajo per a tots dos modes: sala totalment "neutra" + cap
|
||||
// emissor → la pasada no canviaria res al canvas.
|
||||
// subtractive: room=1.0 i sense emissors → pixel * 1.0 global.
|
||||
// additive: room=1.0 i sense emissors → cap aportació.
|
||||
const std::uint8_t ROOM = toByte(room_light);
|
||||
if (ROOM == 255 && emitters.empty()) { return; }
|
||||
|
||||
if (mode == Mode::ADDITIVE) {
|
||||
applyAdditive(canvas, width, height, ROOM, emitters);
|
||||
} else {
|
||||
applySubtractive(canvas, width, height, ROOM, emitters);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Lighting
|
||||
@@ -0,0 +1,150 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
// Sistema d'il·luminació CPU. Treballa sobre el mateix canvas ARGB del
|
||||
// joc (320×240) que tot el pipeline de render: després que GameScene
|
||||
// haja pintat tilemap, paint layer, sombres i actors, una pasada final
|
||||
// modifica cada píxel del canvas en funció de la "llum de la sala" i
|
||||
// dels cercles emissors al voltant de certes entitats.
|
||||
//
|
||||
// Dos modes seleccionables per la sala (`lighting.mode` al YAML):
|
||||
//
|
||||
// SUBTRACTIVE (mode històric, default si no es declara mode):
|
||||
// out = src × max(room_light, max_emitter_intensity_at_pixel)
|
||||
// Els emissors "trenquen" una capa fosca multiplicativa. Tot el
|
||||
// trip cromàtic dels emissors s'ignora — només la intensitat
|
||||
// compta. Pensat per a sales fosques (cripta, pirámide).
|
||||
//
|
||||
// ADDITIVE (mode nou):
|
||||
// out = saturate( (src × room_light) + Σ (emitter_color × intensity) )
|
||||
// El canvas s'enfosqueix opcionalment per `room_light` (atenuació
|
||||
// ambient) i els emissors hi sumen color cap a sobre. Les intensitats
|
||||
// dels anells es llegeixen com a "alfa" del color afegit (0..1).
|
||||
// Pensat per a sales clares on les antorxes aporten halo cromàtic
|
||||
// sense restar contrast — o per a sales lleument fosques (0.85, p.ex.)
|
||||
// on el halo de l'antorxa es veu càlid en lloc de "blanquinós".
|
||||
//
|
||||
// Forma del cercle de cada emissor (radial, 3 anells configurables al
|
||||
// fitxer data/config/lighting.yaml; veure `GradientConfig`):
|
||||
// 0..inner_ratio del radi → inner_intensity
|
||||
// inner_ratio..mid_ratio → mid_intensity
|
||||
// mid_ratio..1.0 → outer_intensity
|
||||
//
|
||||
// La corba viu en dos blocs separats al YAML perquè la mateixa escala
|
||||
// 0..1 té significats molt diferents per mode:
|
||||
// `gradient:` → corba del mode subtractive (1.0 = "no fosqueix")
|
||||
// `gradient_additive:` → corba del mode additive (1.0 = "afegeix
|
||||
// el color sencer i satura"). Es vol molt més
|
||||
// baixa per a no inundar la sala de color.
|
||||
// Cada mode llegeix la seva pròpia corba — no es comparteixen valors.
|
||||
//
|
||||
// Combinació entre emissors:
|
||||
// SUBTRACTIVE → per a cada píxel es pren el màxim de la intensitat
|
||||
// aportada per cada emissor que el solapa (no se sumen intensitats).
|
||||
// ADDITIVE → per a cada píxel se sumen totes les contribucions
|
||||
// (`color × intensity`), saturant a 255 per canal. Dues llums de
|
||||
// colors diferents que solapen produeixen la suma natural (orange +
|
||||
// blue → near-white) — comportament físic.
|
||||
namespace Lighting {
|
||||
|
||||
// Mode de composició. La sala el selecciona via `lighting.mode:
|
||||
// additive | subtractive` (default subtractive si manca, retrocompat).
|
||||
enum class Mode : std::uint8_t {
|
||||
SUBTRACTIVE,
|
||||
ADDITIVE,
|
||||
};
|
||||
|
||||
// Color emès. Default blanc (sense modificació quan s'usa en mode
|
||||
// additive). En mode subtractive s'ignora — la pasada multiplicativa
|
||||
// només té un canal (intensitat). El YAML accepta `color: [r, g, b]`
|
||||
// a qualsevol nivell de la cadena de resolució (tipus, sala, spawn).
|
||||
struct Color {
|
||||
std::uint8_t r{255};
|
||||
std::uint8_t g{255};
|
||||
std::uint8_t b{255};
|
||||
};
|
||||
|
||||
// Forma del gradient. Compartida per tots els emissors per evitar
|
||||
// duplicar la corba a cada YAML d'entitat. Càrrega via loadConfig.
|
||||
struct GradientConfig {
|
||||
float inner_ratio{0.8F};
|
||||
float mid_ratio{0.9F};
|
||||
float inner_intensity{1.0F};
|
||||
float mid_intensity{0.9F};
|
||||
float outer_intensity{0.8F};
|
||||
};
|
||||
|
||||
// Paràmetres del flicker per a emissors amb `flicker=true`. La
|
||||
// forma final del radi és la suma d'una sinusoide "respiració"
|
||||
// (wave_*) i una sinusoide secundària de freqüència més alta
|
||||
// (noise_*) que aporta la sensació d'inquietud. Les fases es
|
||||
// deriven de la seed estable de cada emissor — sense estat per
|
||||
// emissor. Les amplituds són **ràtios** sobre el radi base (p.ex.
|
||||
// 0.05 = 5% del radi): així una torxa de radi 40 i una de radi 24
|
||||
// tenen el mateix percentatge d'oscil·lació visual, no els mateixos
|
||||
// píxels (que es notarien més en la petita).
|
||||
struct FlickerConfig {
|
||||
float wave_amplitude_ratio{0.05F};
|
||||
float wave_frequency_hz{3.0F};
|
||||
float noise_amplitude_ratio{0.025F};
|
||||
float noise_frequency_hz{7.0F};
|
||||
};
|
||||
|
||||
// Posició al canvas (en píxels, mateixa unitat que la resta del
|
||||
// motor) i radi efectiu de l'emissor. El centre és el del sprite
|
||||
// visible: és l'entitat qui sap on cau (veure `Entity::getLightCenter`).
|
||||
// `flicker` activa la modulació del radi per frame; `seed` és un
|
||||
// identificador estable per emissor (no canvia amb la posició) del
|
||||
// qual es deriva la fase del flicker — així els emissors mòbils
|
||||
// mantenen un patró propi que no "deriva" pel moviment. `color`
|
||||
// només té efecte en mode additive.
|
||||
struct Emitter {
|
||||
float cx{0.0F};
|
||||
float cy{0.0F};
|
||||
int radius{0};
|
||||
int seed{0};
|
||||
bool flicker{false};
|
||||
Color color{};
|
||||
// Paràmetres del flicker resolts per a aquest emissor (combinació
|
||||
// global + override del tipus/sala/spawn). Només té sentit si
|
||||
// `flicker == true`; en cas contrari s'ignoren. Per defecte
|
||||
// s'inicialitza amb els defaults compilats — el caller (game_scene)
|
||||
// hi posa el resolt correcte.
|
||||
FlickerConfig flicker_params{};
|
||||
};
|
||||
|
||||
// Llegeix `yaml_path` (data/config/lighting.yaml) i pobla la
|
||||
// configuració global. Qualsevol clau absent cau als Defaults::Lighting.
|
||||
// No es crida més d'una vegada en un run normal; tornar-la a cridar
|
||||
// simplement reemplaça la configuració activa.
|
||||
void loadConfig(const std::string& yaml_path);
|
||||
|
||||
// Configuració activa (per a debug/inspectors). Retorna sempre la
|
||||
// corba subtractive — la corba additive viu en paral·lel al cpp i,
|
||||
// si algun dia es necessita inspeccionar des de fora, caldrà afegir
|
||||
// una variant `configAdditive()`. Inicialitzada amb els
|
||||
// Defaults::Lighting fins que loadConfig() es crida.
|
||||
[[nodiscard]] auto config() -> const GradientConfig&;
|
||||
|
||||
// Defaults globals del flicker (data/config/lighting.yaml, bloc
|
||||
// `flicker:`). Els callers del construct path d'entitats el llegeixen
|
||||
// per inicialitzar la `FlickerConfig` de cada emissor, i aplicar a
|
||||
// sobre els overrides per tipus/sala/spawn.
|
||||
[[nodiscard]] auto flickerConfig() -> const FlickerConfig&;
|
||||
|
||||
// Aplica la pasada d'il·luminació al canvas in-place. `mode`
|
||||
// selecciona la composició (vegeu el comentari de la capçalera).
|
||||
// `room_light` és l'atenuació ambient de la sala (0..1, 1 = sense
|
||||
// atenuació): en subtractive és el "terra" de la intensitat fora
|
||||
// dels emissors; en additive és la multiplicació prèvia del canvas
|
||||
// abans de sumar els emissors (es pot deixar a 1.0 per "només tint",
|
||||
// o baixar per combinar oscuriment ambient + halo cromàtic). El
|
||||
// span d'emissors pot ser buit. La funció no fa cap allocació si
|
||||
// es crida amb la mateixa mida de canvas múltiples cops: manté un
|
||||
// buffer intern reutilitzable (només l'usa el mode subtractive).
|
||||
void apply(std::uint32_t* canvas, int width, int height, Mode mode, float room_light, std::span<const Emitter> emitters);
|
||||
|
||||
} // namespace Lighting
|
||||
@@ -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(),
|
||||
|
||||