Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d26abcb9d5 | ||
|
|
a358d8ddbd | ||
|
|
f7c4d62234 | ||
|
|
06a49e09ba | ||
|
|
629baaaec3 | ||
|
|
932d195eb0 | ||
|
|
3e31a88f78 | ||
|
|
cba46642c6 | ||
|
|
8c84892250 | ||
|
|
d14da0e61f | ||
|
|
8bdc994b75 | ||
|
|
6a70d6de9f | ||
|
|
2523661c8f | ||
|
|
fa27308897 | ||
|
|
e58eb10d6d | ||
|
|
a29ea641ab | ||
|
|
28ff425c89 |
No files matched your search
@@ -94,6 +94,7 @@ set(APP_SOURCES
|
|||||||
source/core/rendering/sprite/animated_sprite_parser.cpp
|
source/core/rendering/sprite/animated_sprite_parser.cpp
|
||||||
source/core/rendering/effects/heat_haze.cpp
|
source/core/rendering/effects/heat_haze.cpp
|
||||||
source/core/rendering/effects/scan_reveal.cpp
|
source/core/rendering/effects/scan_reveal.cpp
|
||||||
|
source/core/rendering/effects/shadow.cpp
|
||||||
source/core/rendering/transitions/mosaic.cpp
|
source/core/rendering/transitions/mosaic.cpp
|
||||||
source/core/rendering/transitions/fade_to_black.cpp
|
source/core/rendering/transitions/fade_to_black.cpp
|
||||||
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
||||||
|
|||||||
@@ -293,8 +293,14 @@ rebuild: clean all
|
|||||||
# Empaqueta data/ a $(BUILDDIR)/resources.pack (format AE26). Força un rebuild
|
# 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
|
# 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`.
|
# 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:
|
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
|
@cmake --build $(BUILDDIR) --target pack_resources
|
||||||
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
||||||
|
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ colors:
|
|||||||
menu_cursor: 71
|
menu_cursor: 71
|
||||||
menu_disabled: 16
|
menu_disabled: 16
|
||||||
hud_bg: 9
|
hud_bg: 9
|
||||||
hud_label: 58
|
hud_flash: 63
|
||||||
hud_value: 187
|
|
||||||
hud_flash: 237
|
|
||||||
# Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). Independents
|
# Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO). Independents
|
||||||
# de notification_* perquè la fase i les notificacions són dos canals
|
# de notification_* perquè la fase i les notificacions són dos canals
|
||||||
# visuals distints: ací pots canviar el color del rètol sense tocar
|
# visuals distints: ací pots canviar el color del rètol sense tocar
|
||||||
# els missatges del HUD.
|
# els missatges del HUD. Comparteix la família del CENTER del HUD
|
||||||
room_text: 160
|
# (or sobre marró) perquè "Piràmide N / Sala N" sigui sempre daurat
|
||||||
room_text_outline: 253
|
# arreu del joc.
|
||||||
|
room_text: 55 # or brillant (#FFDA00, = hud_center.value)
|
||||||
|
room_text_outline: 10 # marró cremat (#482400, = hud_center.shadow)
|
||||||
|
|
||||||
timings:
|
timings:
|
||||||
notification_ms: 4000
|
notification_ms: 4000
|
||||||
@@ -74,6 +74,24 @@ hud:
|
|||||||
flash_ms: 250 # durada del flash quan un jugador rep un hit
|
flash_ms: 250 # durada del flash quan un jugador rep un hit
|
||||||
shake_ms: 300 # durada del shake que acompanya el flash
|
shake_ms: 300 # durada del shake que acompanya el flash
|
||||||
|
|
||||||
|
# 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
|
# 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
|
# 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
|
# independentment de la mida de la caixa). El padding vertical és asimètric a
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 306 B After Width: | Height: | Size: 303 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -36,18 +36,17 @@ animations:
|
|||||||
walk_north: { frames: [4, 5, 6, 7], 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_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_west: { frames: [8, 9, 10, 11], fps: 8, loop: true, flip_h: true }
|
||||||
|
raise: { frames: [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27], fps: 8 , loop: false }
|
||||||
ai:
|
ai:
|
||||||
type: corridor_wanderer
|
type: corridor_wanderer
|
||||||
direction_choice: random
|
direction_choice: random
|
||||||
initial_direction: east
|
initial_direction: east
|
||||||
|
|
||||||
# Estat SPAWN: la mòmia s'instancia visible al pixel_rect_ inicial just
|
# Estat SPAWN: la mòmia s'instancia al pixel_rect_ inicial quan
|
||||||
# quan GameScene crida `spawnPendingActors()` (a la subfase text-exit
|
# GameScene crida `spawnPendingActors()` (subfase text-exit de
|
||||||
# de ROOM_INTRO, fase 4). El `triggerSpawnExit` es dispara llavors
|
# ROOM_INTRO, fase 4) amb l'animació `raise` activa. La duració del
|
||||||
# mateix; passats `duration_s` segons transita a ROAMING i empieza a
|
# SPAWN la marca la pròpia animació (no-loop): al `completed()`,
|
||||||
# deambular. Sense aquest bloc, default 0 ⇒ ACTIVE immediat.
|
# transita a ROAMING i comença a deambular.
|
||||||
spawn:
|
|
||||||
duration_s: 1.0
|
|
||||||
stats:
|
stats:
|
||||||
speed: 30
|
speed: 30
|
||||||
damage: 1
|
damage: 1
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ animations:
|
|||||||
stun_east: { frames: [18] }
|
stun_east: { frames: [18] }
|
||||||
stun_west: { frames: [18], flip_h: true }
|
stun_west: { frames: [18], flip_h: true }
|
||||||
|
|
||||||
walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
walk_south: { frames: [1, 2, 3, 0], frame_ms: 100 }
|
||||||
walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
walk_north: { frames: [5, 6, 7, 4], frame_ms: 100 }
|
||||||
walk_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
walk_east: { frames: [9, 10, 11, 8], frame_ms: 100 }
|
||||||
walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
walk_west: { frames: [9, 10, 11, 8], frame_ms: 100, flip_h: true }
|
||||||
|
|
||||||
hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
||||||
hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
||||||
@@ -97,13 +97,10 @@ turn:
|
|||||||
south_to_north: west
|
south_to_north: west
|
||||||
|
|
||||||
# Salt vertical (px de z, no toca la posició al món).
|
# 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:
|
jump:
|
||||||
impulse: 200
|
impulse: 200
|
||||||
gravity: 600
|
gravity: 600
|
||||||
landing_stun_ms: 400
|
landing_stun_ms: 400
|
||||||
shadow_y_offset: 3
|
|
||||||
sound: jump.wav
|
sound: jump.wav
|
||||||
land_sound: land.wav
|
land_sound: land.wav
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ animations:
|
|||||||
stun_east: { frames: [18] }
|
stun_east: { frames: [18] }
|
||||||
stun_west: { frames: [18], flip_h: true }
|
stun_west: { frames: [18], flip_h: true }
|
||||||
|
|
||||||
walk_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
walk_south: { frames: [1, 2, 3, 0], frame_ms: 100 }
|
||||||
walk_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
walk_north: { frames: [5, 6, 7, 4], frame_ms: 100 }
|
||||||
walk_east: { frames: [8, 9, 10, 11], frame_ms: 100 }
|
walk_east: { frames: [9, 10, 11, 8], frame_ms: 100 }
|
||||||
walk_west: { frames: [8, 9, 10, 11], frame_ms: 100, flip_h: true }
|
walk_west: { frames: [9, 10, 11, 8], frame_ms: 100, flip_h: true }
|
||||||
|
|
||||||
hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
hurt_south: { frames: [0, 1, 2, 3], frame_ms: 100 }
|
||||||
hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
hurt_north: { frames: [4, 5, 6, 7], frame_ms: 100 }
|
||||||
@@ -97,13 +97,10 @@ turn:
|
|||||||
south_to_north: west
|
south_to_north: west
|
||||||
|
|
||||||
# Salt vertical (px de z, no toca la posició al món).
|
# 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:
|
jump:
|
||||||
impulse: 200
|
impulse: 200
|
||||||
gravity: 600
|
gravity: 600
|
||||||
landing_stun_ms: 400
|
landing_stun_ms: 400
|
||||||
shadow_y_offset: 3
|
|
||||||
sound: jump.wav
|
sound: jump.wav
|
||||||
land_sound: land.wav
|
land_sound: land.wav
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 1.6 KiB |
@@ -20,11 +20,23 @@ flags:
|
|||||||
interactable: true
|
interactable: true
|
||||||
animations:
|
animations:
|
||||||
closed: { frames: [0], fps: 1, loop: true }
|
closed: { frames: [0], fps: 1, loop: true }
|
||||||
opening: { frames: [0], fps: 6, loop: false }
|
opening: { frames: [1, 2], fps: 6, loop: false }
|
||||||
open: { frames: [0], fps: 1, loop: true }
|
open: { frames: [3], fps: 1, loop: true }
|
||||||
behaviour:
|
behaviour:
|
||||||
states: [CLOSED, OPENING, OPEN]
|
states: [CLOSED, OPENING, OPEN]
|
||||||
initial_state: CLOSED
|
initial_state: CLOSED
|
||||||
on_perimeter_complete: OPENING
|
on_perimeter_complete: OPENING
|
||||||
on_opening_end: OPEN
|
on_opening_end: OPEN
|
||||||
emits_event_on_open: sarcophagus_opened
|
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
|
||||||
|
After Width: | Height: | Size: 98 B |
@@ -14,7 +14,7 @@ input:
|
|||||||
shader_off: "Shaders desactivats"
|
shader_off: "Shaders desactivats"
|
||||||
vsync_on: "VSync activat"
|
vsync_on: "VSync activat"
|
||||||
vsync_off: "VSync desactivat"
|
vsync_off: "VSync desactivat"
|
||||||
screenshot: "Captura"
|
screenshot: "Imatge {} guardada a {}"
|
||||||
shader: "Shader"
|
shader: "Shader"
|
||||||
preset: "Preset"
|
preset: "Preset"
|
||||||
scaling_disabled: "Escalat sense"
|
scaling_disabled: "Escalat sense"
|
||||||
@@ -41,10 +41,10 @@ hud:
|
|||||||
# Prompts mostrats al bloc d'un slot inactiu (en lloc dels zeros)
|
# Prompts mostrats al bloc d'un slot inactiu (en lloc dels zeros)
|
||||||
# quan un jugador encara no ha pulsat START o ha esgotat vides.
|
# quan un jugador encara no ha pulsat START o ha esgotat vides.
|
||||||
# Dues línies per fer-los caber al bloc lateral (~106 px).
|
# Dues línies per fer-los caber al bloc lateral (~106 px).
|
||||||
join_prompt_can_join_l1: "POLSA START"
|
join_prompt_can_join_l1: "Polsa start"
|
||||||
join_prompt_can_join_l2: "PER JUGAR"
|
join_prompt_can_join_l2: "per jugar"
|
||||||
join_prompt_wait_l1: "ESPERA"
|
join_prompt_wait_l1: "Espera"
|
||||||
join_prompt_wait_l2: "PER JUGAR"
|
join_prompt_wait_l2: "per jugar"
|
||||||
|
|
||||||
# Textos de l'escena TITLE.
|
# Textos de l'escena TITLE.
|
||||||
title:
|
title:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ input:
|
|||||||
shader_off: "Shaders off"
|
shader_off: "Shaders off"
|
||||||
vsync_on: "VSync on"
|
vsync_on: "VSync on"
|
||||||
vsync_off: "VSync off"
|
vsync_off: "VSync off"
|
||||||
screenshot: "Screenshot"
|
screenshot: "Image {} saved at {}"
|
||||||
shader: "Shader"
|
shader: "Shader"
|
||||||
preset: "Preset"
|
preset: "Preset"
|
||||||
scaling_disabled: "Scaling off"
|
scaling_disabled: "Scaling off"
|
||||||
@@ -41,10 +41,10 @@ hud:
|
|||||||
# Prompts shown in an inactive slot's block (instead of zeros) while
|
# 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
|
# 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).
|
# short lines so they fit in the side block (~106 px wide).
|
||||||
join_prompt_can_join_l1: "PRESS START"
|
join_prompt_can_join_l1: "Press start"
|
||||||
join_prompt_can_join_l2: "TO PLAY"
|
join_prompt_can_join_l2: "to play"
|
||||||
join_prompt_wait_l1: "PLEASE"
|
join_prompt_wait_l1: "Please"
|
||||||
join_prompt_wait_l2: "WAIT"
|
join_prompt_wait_l2: "wait"
|
||||||
|
|
||||||
# Texts of the TITLE scene.
|
# Texts of the TITLE scene.
|
||||||
title:
|
title:
|
||||||
|
|||||||
@@ -43,8 +43,11 @@ text:
|
|||||||
# text i decreix amb l'exit_curve. `color` són tres canals RGB (0..255);
|
# 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).
|
# `alpha` és la transparència del blend (0 = invisible, 255 = opac).
|
||||||
text_panel:
|
text_panel:
|
||||||
color: [36, 182, 170]
|
# Violeta cerimonial: contrasta amb l'or del text (room_text), no
|
||||||
alpha: 128
|
# 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
|
height_px: 32
|
||||||
|
|
||||||
# Sub-FSM de game over: dispara CONTINUE? quan tots els jugadors
|
# Sub-FSM de game over: dispara CONTINUE? quan tots els jugadors
|
||||||
|
|||||||
@@ -136,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
|
// 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
|
// 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.
|
// sec — l'usuari sent el so encara que la cua no s'apliqui.
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ class Audio {
|
|||||||
// --- Control de sons ---
|
// --- Control de sons ---
|
||||||
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
|
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
|
||||||
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
|
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
|
||||||
|
// Reprodueix un so 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
|
// Reprodueix un so processat per un efecte definit a data/config/sounds.yaml
|
||||||
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix
|
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix
|
||||||
// o el motor està al cap de canals amb efecte, fa fallback a playSound
|
// o el motor està al cap de canals amb efecte, fa fallback a playSound
|
||||||
|
|||||||
@@ -430,6 +430,12 @@ namespace Ja {
|
|||||||
return channel;
|
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) {
|
void Engine::pauseChannel(const int channel) {
|
||||||
forEachTargetChannel(channel, [](Channel& ch) {
|
forEachTargetChannel(channel, [](Channel& ch) {
|
||||||
if (ch.state == ChannelState::PLAYING) {
|
if (ch.state == ChannelState::PLAYING) {
|
||||||
|
|||||||
@@ -192,6 +192,14 @@ namespace Ja {
|
|||||||
// --- So ---
|
// --- So ---
|
||||||
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
|
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
|
||||||
auto playSoundOnChannel(Sound* sound, int channel, 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
|
// Reproducció amb so processat per un efecte. Retorna el canal
|
||||||
// assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS).
|
// 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
|
// El sound original només s'usa per consultar el spec/buffer; el
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <format>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/input/key_config.hpp"
|
#include "core/input/key_config.hpp"
|
||||||
@@ -69,15 +70,26 @@ namespace GlobalInputs {
|
|||||||
notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off");
|
notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Volca el canvas ARGB actual a PNG dins de `screenshots/`. Si ha
|
// Volca el canvas ARGB actual a PNG dins del directori de
|
||||||
// fallat l'escriptura, el handler no mostra notificació (retorn buit).
|
// 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() {
|
void onScreenshot() {
|
||||||
const auto* buffer = Screen::get().getCanvasArgb();
|
const auto* buffer = Screen::get().getCanvasArgb();
|
||||||
const int W = Screen::get().getCanvasWidth();
|
const int W = Screen::get().getCanvasWidth();
|
||||||
const int H = Screen::get().getCanvasHeight();
|
const int H = Screen::get().getCanvasHeight();
|
||||||
const std::string FILENAME = Screenshot::save(buffer, W, H);
|
const auto R = Screenshot::save(buffer, W, H);
|
||||||
if (!FILENAME.empty()) {
|
if (!R.filename.empty()) {
|
||||||
Notification::notify(Locale::get().text("input.screenshot") + ' ' + FILENAME);
|
// `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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -12,11 +12,52 @@
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Parteix `text` en una llista de línies que caben cadascuna dins
|
// Parteix `text` en una llista de línies que caben cadascuna dins
|
||||||
// `max_width`. Algoritme de projecte_2026: tall per espais, la paraula
|
// Trenca una "paraula" llarga (sense espais) en trossos contigus
|
||||||
// que no cap passa a la línia següent. Les paraules aïllades més
|
// que càpiguen a `max_width`. Si un sol caràcter ja excedeix
|
||||||
// llargues que `max_width` es queden a la seva pròpia línia sense
|
// (font enorme), el deixa sol per garantir progrés. La part final
|
||||||
// trencar-les (cas improbable amb els pretty_names de gamepad retallats
|
// que encara cap queda a `current` per a fusionar-se amb paraules
|
||||||
// a ~25 caràcters).
|
// posteriors via espai.
|
||||||
|
void hardWrapWord(const std::string& word, int max_width, const Text& font, int kerning, std::vector<std::string>& lines, std::string& current) {
|
||||||
|
std::string buf;
|
||||||
|
for (char c : word) {
|
||||||
|
std::string candidate = buf;
|
||||||
|
candidate += c;
|
||||||
|
if (buf.empty() || font.length(candidate, kerning) <= max_width) {
|
||||||
|
buf = std::move(candidate);
|
||||||
|
} else {
|
||||||
|
lines.push_back(buf);
|
||||||
|
buf = std::string(1, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = std::move(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Posa la `word` a la `current` line si hi cap (amb separador
|
||||||
|
// implícit si current no està buida), o fa flush + reintenta a
|
||||||
|
// línia neta. Si tampoc cap sola, hard-wrap caràcter a caràcter.
|
||||||
|
void appendWord(const std::string& word, int max_width, const Text& font, int kerning, std::vector<std::string>& lines, std::string& current) {
|
||||||
|
if (!current.empty()) {
|
||||||
|
std::string candidate = current;
|
||||||
|
candidate += ' ';
|
||||||
|
candidate += word;
|
||||||
|
if (font.length(candidate, kerning) <= max_width) {
|
||||||
|
current = std::move(candidate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lines.push_back(current);
|
||||||
|
current.clear();
|
||||||
|
}
|
||||||
|
if (font.length(word, kerning) <= max_width) {
|
||||||
|
current = word;
|
||||||
|
} else {
|
||||||
|
hardWrapWord(word, max_width, font, kerning, lines, current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `max_width`. Tall per espais; les paraules més llargues que
|
||||||
|
// `max_width` (rutes, URLs...) es trenquen caràcter a caràcter
|
||||||
|
// — sense això, una caixa que excedís el canvas faria que
|
||||||
|
// `fillRect` rebés rangs invertits i petés.
|
||||||
auto wrapToWidth(const std::string& text, int max_width, const Text& font, int kerning) -> std::vector<std::string> {
|
auto wrapToWidth(const std::string& text, int max_width, const Text& font, int kerning) -> std::vector<std::string> {
|
||||||
std::vector<std::string> lines;
|
std::vector<std::string> lines;
|
||||||
if (text.empty()) {
|
if (text.empty()) {
|
||||||
@@ -30,26 +71,12 @@ namespace {
|
|||||||
const std::size_t SPACE_POS = text.find(' ', i);
|
const std::size_t SPACE_POS = text.find(' ', i);
|
||||||
const bool LAST = (SPACE_POS == std::string::npos);
|
const bool LAST = (SPACE_POS == std::string::npos);
|
||||||
const std::string WORD = text.substr(i, LAST ? std::string::npos : SPACE_POS - i);
|
const std::string WORD = text.substr(i, LAST ? std::string::npos : SPACE_POS - i);
|
||||||
|
appendWord(WORD, max_width, font, kerning, lines, current);
|
||||||
if (current.empty()) {
|
|
||||||
current = WORD;
|
|
||||||
} else {
|
|
||||||
std::string candidate = current;
|
|
||||||
candidate += ' ';
|
|
||||||
candidate += WORD;
|
|
||||||
if (font.length(candidate, kerning) <= max_width) {
|
|
||||||
current = std::move(candidate);
|
|
||||||
} else {
|
|
||||||
lines.push_back(current);
|
|
||||||
current = WORD;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LAST) { break; }
|
if (LAST) { break; }
|
||||||
i = SPACE_POS + 1;
|
i = SPACE_POS + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push_back(current);
|
if (!current.empty()) { lines.push_back(current); }
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,12 @@ void Screen::fillRect(int x, int y, int w, int h, std::uint32_t argb) {
|
|||||||
const int Y0 = std::max(y, CLIP.y);
|
const int Y0 = std::max(y, CLIP.y);
|
||||||
const int X1 = std::min(x + w, CLIP.x + CLIP.w);
|
const int X1 = std::min(x + w, CLIP.x + CLIP.w);
|
||||||
const int Y1 = std::min(y + h, CLIP.y + CLIP.h);
|
const int Y1 = std::min(y + h, CLIP.y + CLIP.h);
|
||||||
|
// Bail-out defensiu: si el rang és buit o invertit després del clip,
|
||||||
|
// `std::fill(first, last)` amb `first > last` és UB i a la pràctica
|
||||||
|
// entra en bucle infinit (vist en un crash a l'overflow del Notifier
|
||||||
|
// quan la caixa quedava més ampla que el canvas). El `blendRect`
|
||||||
|
// d'aquí baix ja és segur perquè usa `for(ix; ix < X1; ...)`.
|
||||||
|
if (X1 <= X0 || Y1 <= Y0) { return; }
|
||||||
for (int iy = Y0; iy < Y1; ++iy) {
|
for (int iy = Y0; iy < Y1; ++iy) {
|
||||||
std::uint32_t* row = canvas_.data() + (static_cast<std::ptrdiff_t>(iy) * canvas_width_);
|
std::uint32_t* row = canvas_.data() + (static_cast<std::ptrdiff_t>(iy) * canvas_width_);
|
||||||
std::fill(row + X0, row + X1, argb);
|
std::fill(row + X0, row + X1, argb);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "external/stb_image_write.h"
|
#include "external/stb_image_write.h"
|
||||||
@@ -13,21 +14,18 @@ namespace Screenshot {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Crea la carpeta `screenshots/` si no existeix i torna la ruta
|
// Estat de mòdul: ruta base on s'escriu la subcarpeta
|
||||||
// completa al fitxer. El paràmetre `filename` rep el nom nu (sense
|
// `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback
|
||||||
// directori) per poder mostrar-lo a la notificació.
|
// si el Director no ha pogut establir la ruta per plataforma).
|
||||||
auto generateFilePath(std::string& filename) -> std::string {
|
std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables)
|
||||||
std::filesystem::create_directories("screenshots");
|
|
||||||
|
|
||||||
const auto NOW = std::chrono::system_clock::now();
|
// Construeix la ruta del directori on van les captures:
|
||||||
const std::time_t TIME = std::chrono::system_clock::to_time_t(NOW);
|
// `<base>/screenshots/`. Si la base és buida, retorna
|
||||||
const std::tm* tm = std::localtime(&TIME);
|
// "screenshots/" relativa al CWD.
|
||||||
|
auto screenshotsDir() -> std::string {
|
||||||
char timestamp[20];
|
if (g_base_dir.empty()) { return "screenshots/"; }
|
||||||
std::strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm);
|
const bool ENDS_WITH_SEP = (g_base_dir.back() == '/' || g_base_dir.back() == '\\');
|
||||||
|
return g_base_dir + (ENDS_WITH_SEP ? "" : "/") + "screenshots/";
|
||||||
filename = std::string("scr_") + timestamp + ".png";
|
|
||||||
return "screenshots/" + filename;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converteix ARGB8888 → RGBA8888 in-place i escriu el PNG. stb_image_write
|
// Converteix ARGB8888 → RGBA8888 in-place i escriu el PNG. stb_image_write
|
||||||
@@ -47,14 +45,28 @@ namespace Screenshot {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto save(const std::uint32_t* buffer, int width, int height) -> std::string {
|
void setBaseDir(std::string dir) {
|
||||||
|
g_base_dir = std::move(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto save(const std::uint32_t* buffer, int width, int height) -> Result {
|
||||||
|
const std::string FOLDER = screenshotsDir();
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(FOLDER, ec);
|
||||||
|
|
||||||
|
const auto NOW = std::chrono::system_clock::now();
|
||||||
|
const std::time_t TIME = std::chrono::system_clock::to_time_t(NOW);
|
||||||
|
const std::tm* tm = std::localtime(&TIME);
|
||||||
|
char timestamp[20];
|
||||||
|
std::strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm);
|
||||||
|
|
||||||
|
const std::string FILENAME = std::string("scr_") + timestamp + ".png";
|
||||||
|
const std::string FILEPATH = FOLDER + FILENAME;
|
||||||
|
|
||||||
// Còpia local per no tocar el canvas original durant la conversió.
|
// Còpia local per no tocar el canvas original durant la conversió.
|
||||||
std::vector<std::uint32_t> copy(buffer, buffer + (static_cast<size_t>(width) * height));
|
std::vector<std::uint32_t> copy(buffer, buffer + (static_cast<size_t>(width) * height));
|
||||||
|
if (!writePng(copy.data(), width, height, FILEPATH)) { return {}; }
|
||||||
std::string filename;
|
return {.filename = FILENAME, .folder = FOLDER};
|
||||||
const std::string FILEPATH = generateFilePath(filename);
|
|
||||||
|
|
||||||
return writePng(copy.data(), width, height, FILEPATH) ? filename : "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Screenshot
|
} // namespace Screenshot
|
||||||
@@ -3,11 +3,30 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
// Volca un buffer ARGB8888 a un fitxer PNG dins de la carpeta `screenshots/`
|
// Volca un buffer ARGB8888 a un fitxer PNG dins de la subcarpeta
|
||||||
// (relativa al CWD). El nom es genera amb un timestamp i la funció retorna
|
// `screenshots/` del directori configurat amb `setBaseDir`. El nom es
|
||||||
// el nom del fitxer creat, o una cadena buida si l'escriptura ha fallat.
|
// genera amb un timestamp i la funció retorna el basename i la ruta
|
||||||
|
// del directori on s'ha escrit (o un Result amb `filename` buit si
|
||||||
|
// l'escriptura ha fallat).
|
||||||
namespace Screenshot {
|
namespace Screenshot {
|
||||||
|
|
||||||
auto save(const std::uint32_t* buffer, int width, int height) -> std::string;
|
// Configura el directori base on s'escriuran les captures (s'hi
|
||||||
|
// crea una subcarpeta `screenshots/`). El Director el crida una
|
||||||
|
// vegada al boot amb `system_folder_` perquè les captures vagin
|
||||||
|
// al mateix lloc per plataforma que la resta de fitxers generats
|
||||||
|
// (config.yaml, gamepad_configs.yaml, shaders/, etc.). Si no es
|
||||||
|
// crida, el fallback és el CWD — comportament antic.
|
||||||
|
void setBaseDir(std::string dir);
|
||||||
|
|
||||||
|
// Resultat d'una captura: el `filename` és el basename del PNG
|
||||||
|
// (per a notificacions/títol) i el `folder` és la ruta absoluta
|
||||||
|
// on s'ha guardat (per a poder dir "guardada a {folder}"). Si
|
||||||
|
// l'escriptura falla, ambdós camps tornen buits.
|
||||||
|
struct Result {
|
||||||
|
std::string filename;
|
||||||
|
std::string folder;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto save(const std::uint32_t* buffer, int width, int height) -> Result;
|
||||||
|
|
||||||
} // namespace Screenshot
|
} // namespace Screenshot
|
||||||
@@ -18,9 +18,11 @@
|
|||||||
#include "core/input/key_config.hpp"
|
#include "core/input/key_config.hpp"
|
||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
|
#include "core/rendering/effects/shadow.hpp"
|
||||||
#include "core/rendering/notifier.hpp"
|
#include "core/rendering/notifier.hpp"
|
||||||
#include "core/rendering/render_info.hpp"
|
#include "core/rendering/render_info.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
|
#include "core/rendering/screenshot.hpp"
|
||||||
#include "core/rendering/surface.hpp"
|
#include "core/rendering/surface.hpp"
|
||||||
#include "core/rendering/text.hpp"
|
#include "core/rendering/text.hpp"
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
@@ -162,6 +164,11 @@ Director::Director() {
|
|||||||
Resource::Cache::get().loadSurface(ROOM_TEXT_FONT_GIF_KEY, UiConfig::get().roomTextFont().gif);
|
Resource::Cache::get().loadSurface(ROOM_TEXT_FONT_GIF_KEY, UiConfig::get().roomTextFont().gif);
|
||||||
Resource::Cache::get().getSurface(ROOM_TEXT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
Resource::Cache::get().getSurface(ROOM_TEXT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
||||||
|
|
||||||
|
// Sprite compartit de la sombra (tira horitzontal de fotogrames de
|
||||||
|
// 16x16, de petit a gran). El consumeix `Effects::Shadow::render`,
|
||||||
|
// que el reaprofiten Player i qualsevol entitat futura compatible.
|
||||||
|
Resource::Cache::get().loadSurface(Effects::Shadow::CACHE_KEY, "data/entities/shadow/shadow.gif");
|
||||||
|
|
||||||
// Pistes de música del cicle de sala (intro/playing/outro). Es preloden
|
// Pistes de música del cicle de sala (intro/playing/outro). Es preloden
|
||||||
// ací perquè formen part del preload d'assets, com la font i la paleta;
|
// ací perquè formen part del preload d'assets, com la font i la paleta;
|
||||||
// la lògica de quina sona en cada estat viu a `GameScene`.
|
// la lògica de quina sona en cada estat viu a `GameScene`.
|
||||||
@@ -373,6 +380,12 @@ void Director::createSystemFolder() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
Log::error("Director", "no s'ha pogut crear {}: {}", system_folder_, error.message());
|
Log::error("Director", "no s'ha pogut crear {}: {}", system_folder_, error.message());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Les captures de pantalla viuen sota la mateixa carpeta per
|
||||||
|
// plataforma. Sense aquesta crida, `Screenshot::save` cauria al
|
||||||
|
// CWD del procés (raïz del projecte si llances `build/aee_arcade`
|
||||||
|
// a mà, lloc imprevisible en una release de macOS/Windows).
|
||||||
|
Screenshot::setBaseDir(system_folder_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Director::processEvents() {
|
void Director::processEvents() {
|
||||||
|
|||||||
@@ -82,8 +82,15 @@ UiConfig::UiConfig()
|
|||||||
menu_cursor_(Defaults::UI::MENU_CURSOR),
|
menu_cursor_(Defaults::UI::MENU_CURSOR),
|
||||||
menu_disabled_(Defaults::UI::MENU_DISABLED),
|
menu_disabled_(Defaults::UI::MENU_DISABLED),
|
||||||
hud_bg_(Defaults::UI::HUD_BG),
|
hud_bg_(Defaults::UI::HUD_BG),
|
||||||
hud_label_(Defaults::UI::HUD_LABEL),
|
hud_p1_label_(Defaults::UI::HUD_P1_LABEL),
|
||||||
hud_value_(Defaults::UI::HUD_VALUE),
|
hud_p1_value_(Defaults::UI::HUD_P1_VALUE),
|
||||||
|
hud_p1_shadow_(Defaults::UI::HUD_P1_SHADOW),
|
||||||
|
hud_center_label_(Defaults::UI::HUD_CENTER_LABEL),
|
||||||
|
hud_center_value_(Defaults::UI::HUD_CENTER_VALUE),
|
||||||
|
hud_center_shadow_(Defaults::UI::HUD_CENTER_SHADOW),
|
||||||
|
hud_p2_label_(Defaults::UI::HUD_P2_LABEL),
|
||||||
|
hud_p2_value_(Defaults::UI::HUD_P2_VALUE),
|
||||||
|
hud_p2_shadow_(Defaults::UI::HUD_P2_SHADOW),
|
||||||
hud_flash_(Defaults::UI::HUD_FLASH),
|
hud_flash_(Defaults::UI::HUD_FLASH),
|
||||||
room_text_(Defaults::UI::ROOM_TEXT),
|
room_text_(Defaults::UI::ROOM_TEXT),
|
||||||
room_text_outline_(Defaults::UI::ROOM_TEXT_OUTLINE),
|
room_text_outline_(Defaults::UI::ROOM_TEXT_OUTLINE),
|
||||||
@@ -218,13 +225,26 @@ void UiConfig::load(const std::string& file_path) {
|
|||||||
readColorIndex(c, "menu_cursor", menu_cursor_);
|
readColorIndex(c, "menu_cursor", menu_cursor_);
|
||||||
readColorIndex(c, "menu_disabled", menu_disabled_);
|
readColorIndex(c, "menu_disabled", menu_disabled_);
|
||||||
readColorIndex(c, "hud_bg", hud_bg_);
|
readColorIndex(c, "hud_bg", hud_bg_);
|
||||||
readColorIndex(c, "hud_label", hud_label_);
|
|
||||||
readColorIndex(c, "hud_value", hud_value_);
|
|
||||||
readColorIndex(c, "hud_flash", hud_flash_);
|
readColorIndex(c, "hud_flash", hud_flash_);
|
||||||
readColorIndex(c, "room_text", room_text_);
|
readColorIndex(c, "room_text", room_text_);
|
||||||
readColorIndex(c, "room_text_outline", room_text_outline_);
|
readColorIndex(c, "room_text_outline", room_text_outline_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Famílies de color del HUD: blocs `hud_p1`, `hud_center`,
|
||||||
|
// `hud_p2` amb claus `label`, `value`, `shadow`. Cada bloc i
|
||||||
|
// cada clau són opcionals — el que manqui cau al default
|
||||||
|
// compilat a `Defaults::UI`.
|
||||||
|
auto read_hud_family = [&](const std::string& key, Uint8& label, Uint8& value, Uint8& shadow) {
|
||||||
|
if (!yaml.contains(key)) { return; }
|
||||||
|
const auto& f = yaml[key];
|
||||||
|
readColorIndex(f, "label", label);
|
||||||
|
readColorIndex(f, "value", value);
|
||||||
|
readColorIndex(f, "shadow", shadow);
|
||||||
|
};
|
||||||
|
read_hud_family("hud_p1", hud_p1_label_, hud_p1_value_, hud_p1_shadow_);
|
||||||
|
read_hud_family("hud_center", hud_center_label_, hud_center_value_, hud_center_shadow_);
|
||||||
|
read_hud_family("hud_p2", hud_p2_label_, hud_p2_value_, hud_p2_shadow_);
|
||||||
|
|
||||||
if (yaml.contains("timings")) {
|
if (yaml.contains("timings")) {
|
||||||
const auto& t = yaml["timings"];
|
const auto& t = yaml["timings"];
|
||||||
readField(t, "notification_ms", notification_ms_);
|
readField(t, "notification_ms", notification_ms_);
|
||||||
|
|||||||
@@ -66,8 +66,19 @@ class UiConfig {
|
|||||||
[[nodiscard]] auto menuCursor() const -> Uint8 { return menu_cursor_; }
|
[[nodiscard]] auto menuCursor() const -> Uint8 { return menu_cursor_; }
|
||||||
[[nodiscard]] auto menuDisabled() const -> Uint8 { return menu_disabled_; }
|
[[nodiscard]] auto menuDisabled() const -> Uint8 { return menu_disabled_; }
|
||||||
[[nodiscard]] auto hudBg() const -> Uint8 { return hud_bg_; }
|
[[nodiscard]] auto hudBg() const -> Uint8 { return hud_bg_; }
|
||||||
[[nodiscard]] auto hudLabel() const -> Uint8 { return hud_label_; }
|
// Famílies de color del HUD per bloc (LEFT/CENTER/RIGHT). Cada
|
||||||
[[nodiscard]] auto hudValue() const -> Uint8 { return hud_value_; }
|
// família té label (etiqueta apagada), value (valor destacat) i
|
||||||
|
// shadow (to fosc per a la sombra-embossing). El flash és comú a
|
||||||
|
// les tres famílies (groc neó al guanyar punts).
|
||||||
|
[[nodiscard]] auto hudP1Label() const -> Uint8 { return hud_p1_label_; }
|
||||||
|
[[nodiscard]] auto hudP1Value() const -> Uint8 { return hud_p1_value_; }
|
||||||
|
[[nodiscard]] auto hudP1Shadow() const -> Uint8 { return hud_p1_shadow_; }
|
||||||
|
[[nodiscard]] auto hudCenterLabel() const -> Uint8 { return hud_center_label_; }
|
||||||
|
[[nodiscard]] auto hudCenterValue() const -> Uint8 { return hud_center_value_; }
|
||||||
|
[[nodiscard]] auto hudCenterShadow() const -> Uint8 { return hud_center_shadow_; }
|
||||||
|
[[nodiscard]] auto hudP2Label() const -> Uint8 { return hud_p2_label_; }
|
||||||
|
[[nodiscard]] auto hudP2Value() const -> Uint8 { return hud_p2_value_; }
|
||||||
|
[[nodiscard]] auto hudP2Shadow() const -> Uint8 { return hud_p2_shadow_; }
|
||||||
[[nodiscard]] auto hudFlash() const -> Uint8 { return hud_flash_; }
|
[[nodiscard]] auto hudFlash() const -> Uint8 { return hud_flash_; }
|
||||||
[[nodiscard]] auto roomText() const -> Uint8 { return room_text_; }
|
[[nodiscard]] auto roomText() const -> Uint8 { return room_text_; }
|
||||||
[[nodiscard]] auto roomTextOutline() const -> Uint8 { return room_text_outline_; }
|
[[nodiscard]] auto roomTextOutline() const -> Uint8 { return room_text_outline_; }
|
||||||
@@ -209,8 +220,15 @@ class UiConfig {
|
|||||||
Uint8 menu_cursor_;
|
Uint8 menu_cursor_;
|
||||||
Uint8 menu_disabled_;
|
Uint8 menu_disabled_;
|
||||||
Uint8 hud_bg_;
|
Uint8 hud_bg_;
|
||||||
Uint8 hud_label_;
|
Uint8 hud_p1_label_;
|
||||||
Uint8 hud_value_;
|
Uint8 hud_p1_value_;
|
||||||
|
Uint8 hud_p1_shadow_;
|
||||||
|
Uint8 hud_center_label_;
|
||||||
|
Uint8 hud_center_value_;
|
||||||
|
Uint8 hud_center_shadow_;
|
||||||
|
Uint8 hud_p2_label_;
|
||||||
|
Uint8 hud_p2_value_;
|
||||||
|
Uint8 hud_p2_shadow_;
|
||||||
Uint8 hud_flash_;
|
Uint8 hud_flash_;
|
||||||
Uint8 room_text_;
|
Uint8 room_text_;
|
||||||
Uint8 room_text_outline_;
|
Uint8 room_text_outline_;
|
||||||
|
|||||||
@@ -739,8 +739,10 @@ void Editor::renderEditorHud(const EntityManager& entities) const {
|
|||||||
? "(" + std::to_string(cursor_col_) + "," + std::to_string(cursor_row_) + ")"
|
? "(" + std::to_string(cursor_col_) + "," + std::to_string(cursor_row_) + ")"
|
||||||
: "(--,--)";
|
: "(--,--)";
|
||||||
|
|
||||||
const auto LBL = UiConfig::get().hudLabel();
|
// El HUD ara té tres famílies (P1/CENTER/P2); l'editor n'agafa la
|
||||||
const auto VAL = UiConfig::get().hudValue();
|
// del CENTER (ambre/or) per ser neutra i contrastar amb el mapa.
|
||||||
|
const auto LBL = UiConfig::get().hudCenterLabel();
|
||||||
|
const auto VAL = UiConfig::get().hudCenterValue();
|
||||||
const auto FLASH = UiConfig::get().hudFlash();
|
const auto FLASH = UiConfig::get().hudFlash();
|
||||||
const int KERN = UiConfig::get().hudFont().kerning;
|
const int KERN = UiConfig::get().hudFont().kerning;
|
||||||
// Mateixos Y que el HUD del joc per simetria visual: línia 1 = 4,
|
// Mateixos Y que el HUD del joc per simetria visual: línia 1 = 4,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
|
#include "game/entities/concrete/whip_attack.hpp"
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/entity_manager.hpp"
|
#include "game/entities/entity_manager.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
@@ -65,6 +66,24 @@ namespace {
|
|||||||
// més d'una lane (cas marginal a velocitats típiques, però pot passar
|
// més d'una lane (cas marginal a velocitats típiques, però pot passar
|
||||||
// amb dt grans). Comparació estricta a un costat: no dispara quan la
|
// amb dt grans). Comparació estricta a un costat: no dispara quan la
|
||||||
// caixa ja es troba alineada a la lane (hi està després del snap).
|
// caixa ja es troba alineada a la lane (hi està després del snap).
|
||||||
|
// Mapatge WhipAttack::Direction → Mummy::Direction. Viuen amb noms i
|
||||||
|
// valors paral·lels però són dos enums distints (cada classe modela el
|
||||||
|
// seu propi domini). Conversió 1-a-1; `NONE` no apareix com a possible
|
||||||
|
// sortida del whip, però la deixem per coherència defensiva.
|
||||||
|
auto whipDirToMummyDir(WhipAttack::Direction d) -> Mummy::Direction {
|
||||||
|
switch (d) {
|
||||||
|
case WhipAttack::Direction::NORTH:
|
||||||
|
return Mummy::Direction::NORTH;
|
||||||
|
case WhipAttack::Direction::SOUTH:
|
||||||
|
return Mummy::Direction::SOUTH;
|
||||||
|
case WhipAttack::Direction::EAST:
|
||||||
|
return Mummy::Direction::EAST;
|
||||||
|
case WhipAttack::Direction::WEST:
|
||||||
|
return Mummy::Direction::WEST;
|
||||||
|
}
|
||||||
|
return Mummy::Direction::NONE;
|
||||||
|
}
|
||||||
|
|
||||||
auto findLaneCross(float prev, float delta, float base, float pitch, int max_idx) -> std::optional<float> {
|
auto findLaneCross(float prev, float delta, float base, float pitch, int max_idx) -> std::optional<float> {
|
||||||
const float NEXT = prev + delta;
|
const float NEXT = prev + delta;
|
||||||
if (delta > 0.0F) {
|
if (delta > 0.0F) {
|
||||||
@@ -87,7 +106,8 @@ Mummy::Mummy(const fkyaml::node& yaml)
|
|||||||
: anim_walk_south_(buildAnimation(yaml, "walk_south")),
|
: anim_walk_south_(buildAnimation(yaml, "walk_south")),
|
||||||
anim_walk_north_(buildAnimation(yaml, "walk_north")),
|
anim_walk_north_(buildAnimation(yaml, "walk_north")),
|
||||||
anim_walk_east_(buildAnimation(yaml, "walk_east")),
|
anim_walk_east_(buildAnimation(yaml, "walk_east")),
|
||||||
anim_walk_west_(buildAnimation(yaml, "walk_west")) {
|
anim_walk_west_(buildAnimation(yaml, "walk_west")),
|
||||||
|
anim_raise_(buildAnimation(yaml, "raise")) {
|
||||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
|
||||||
collides_ = true;
|
collides_ = true;
|
||||||
interactable_ = false;
|
interactable_ = false;
|
||||||
@@ -122,16 +142,18 @@ Mummy::Mummy(const fkyaml::node& yaml)
|
|||||||
changeDirection(0.0F, 1.0F);
|
changeDirection(0.0F, 1.0F);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estat SPAWN inicial: la mòmia naix visible i quieta, esperant que el
|
// Estat SPAWN inicial: la mòmia naix en SPAWNING amb l'animació
|
||||||
// GameScene li armi el comptador via `triggerSpawnExit()` (cridat al
|
// `raise` activa (16 frames @ 8fps, no-loop). La duració del SPAWN
|
||||||
// final de la subfase text-exit de ROOM_INTRO). Si el YAML no declara
|
// la marca la pròpia animació — `Mummy::update` la tickeja, i quan
|
||||||
// `spawn.duration_s`, default 0 ⇒ al primer update post-trigger passa
|
// `completed()`, crida `endSpawn()` i restaura la `walk_*` per la
|
||||||
// directament a ACTIVE.
|
// direcció actual. Passem 0 al countdown del DynamicHazard perquè
|
||||||
float spawn_duration_s = 0.0F;
|
// no s'utilitza en aquest cas (el nostre `update` retorna abans).
|
||||||
if (yaml.contains("spawn") && yaml["spawn"].contains("duration_s")) {
|
enterSpawn(0.0F);
|
||||||
spawn_duration_s = yaml["spawn"]["duration_s"].get_value<float>();
|
anim_ = anim_raise_;
|
||||||
|
if (anim_.has_value()) {
|
||||||
|
anim_->reset();
|
||||||
|
flip_ = anim_->flipMode();
|
||||||
}
|
}
|
||||||
enterSpawn(spawn_duration_s);
|
|
||||||
|
|
||||||
// Configuració de l'estat HURT: opcional. Si el bloc no apareix, la
|
// Configuració de l'estat HURT: opcional. Si el bloc no apareix, la
|
||||||
// mòmia no entrarà mai a HURT efectiu (defensiu); el WhipAttack la
|
// mòmia no entrarà mai a HURT efectiu (defensiu); el WhipAttack la
|
||||||
@@ -520,6 +542,15 @@ void Mummy::applyChoice(Direction dir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto Mummy::onWhipHit(WhipAttack* attack) -> bool {
|
||||||
|
// Defensiu: el filtre `shouldReceiveContact` ja salta DynamicHazards en
|
||||||
|
// SPAWN, però blindem aquesta crida si arribara per un camí no central.
|
||||||
|
if (isSpawning()) { return false; }
|
||||||
|
if (!whippable_ || attack == nullptr) { return false; }
|
||||||
|
enterHurt(whipDirToMummyDir(attack->getDirection()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void Mummy::onPlayerContact(Player* player) {
|
void Mummy::onPlayerContact(Player* player) {
|
||||||
// Defensiu: el filtre central a EntityManager::checkPlayerCollisions ja
|
// Defensiu: el filtre central a EntityManager::checkPlayerCollisions ja
|
||||||
// salta DynamicHazards en SPAWN, però blindem la crida directa.
|
// salta DynamicHazards en SPAWN, però blindem la crida directa.
|
||||||
@@ -664,6 +695,21 @@ void Mummy::hurtStep(float delta_time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Mummy::update(float delta_time) {
|
void Mummy::update(float delta_time) {
|
||||||
|
if (isSpawning()) {
|
||||||
|
// SPAWN dirigit per animació: la `raise` és no-loop, així que
|
||||||
|
// `completed()` ens marca el final. Mentre dura, NO cridem
|
||||||
|
// `DynamicHazard::update`: la base hi té el countdown i ja
|
||||||
|
// hauria saltat a ACTIVE (passem `enterSpawn(0)`), i el `step()`
|
||||||
|
// d'IA mouria la mòmia abans de "aixecar-se". En completar,
|
||||||
|
// tornem a la `walk_*` que toca segons la direcció ja triada
|
||||||
|
// al ctor (initial_direction de l'IA).
|
||||||
|
if (anim_.has_value()) { anim_->tick(delta_time); }
|
||||||
|
if (anim_.has_value() && anim_->completed()) {
|
||||||
|
endSpawn();
|
||||||
|
changeDirection(getDirX(), getDirY());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (state_ == State::HURT) {
|
if (state_ == State::HURT) {
|
||||||
// Avancem l'animació inline (no podem cridar `Entity::update` ni
|
// Avancem l'animació inline (no podem cridar `Entity::update` ni
|
||||||
// `DynamicHazard::update`: el primer salta una capa virtual i el
|
// `DynamicHazard::update`: el primer salta una capa virtual i el
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ class Mummy : public DynamicHazard {
|
|||||||
// Contacte amb el jugador: publica `player_hit` amb el dany.
|
// Contacte amb el jugador: publica `player_hit` amb el dany.
|
||||||
void onPlayerContact(Player* player) override;
|
void onPlayerContact(Player* player) override;
|
||||||
|
|
||||||
|
// Contacte amb el fueteig: si la mòmia és whippable, entra a HURT amb
|
||||||
|
// la direcció derivada del whip. Retorna true si efectivament ha
|
||||||
|
// rebut dany — el WhipAttack ho usa per al `already_hit_`. Substitueix
|
||||||
|
// el `dynamic_cast<Mummy*>` que feia el whip abans per discriminar.
|
||||||
|
auto onWhipHit(WhipAttack* attack) -> bool override;
|
||||||
|
|
||||||
// Activa l'estat HURT amb la direcció de fuga preferida `flee_dir`.
|
// Activa l'estat HURT amb la direcció de fuga preferida `flee_dir`.
|
||||||
// Si està bloquejada, prova perpendiculars; si totes estan bloquejades,
|
// Si està bloquejada, prova perpendiculars; si totes estan bloquejades,
|
||||||
// la mòmia queda quieta però en HURT durant tot el timer.
|
// la mòmia queda quieta però en HURT durant tot el timer.
|
||||||
@@ -171,6 +177,12 @@ class Mummy : public DynamicHazard {
|
|||||||
AnimationComponent anim_walk_east_;
|
AnimationComponent anim_walk_east_;
|
||||||
AnimationComponent anim_walk_west_;
|
AnimationComponent anim_walk_west_;
|
||||||
|
|
||||||
|
// Animació d'aparició (no-loop). Mentre `isSpawning()`, `update()`
|
||||||
|
// la tickeja sense moure la mòmia; quan `completed()`, la mòmia
|
||||||
|
// transita a ACTIVE via `endSpawn()` i conmuta a la walk_* que
|
||||||
|
// toca segons direcció.
|
||||||
|
AnimationComponent anim_raise_;
|
||||||
|
|
||||||
// Estat HURT i la seua configuració. `hurt_speed_` és l'efectiva
|
// Estat HURT i la seua configuració. `hurt_speed_` és l'efectiva
|
||||||
// (resultat d'aplicar el `speed` del bloc `hurt:` o, si val 0, la
|
// (resultat d'aplicar el `speed` del bloc `hurt:` o, si val 0, la
|
||||||
// velocitat normal de la mòmia).
|
// velocitat normal de la mòmia).
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
#include "game/entities/concrete/sarcophagus.hpp"
|
#include "game/entities/concrete/sarcophagus.hpp"
|
||||||
|
|
||||||
|
#include <random>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/audio/audio.hpp"
|
||||||
|
#include "core/resources/resource_cache.hpp"
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
|
#include "game/entities/entity_manager.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "utils/defaults.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// RNG local al mòdul, mateix patró que `mummy.cpp`: llavor fixa per
|
||||||
|
// facilitar la depuració. Si algun dia volem aleatoritat real entre
|
||||||
|
// partides, prou amb llegir-la de std::random_device.
|
||||||
|
auto rng() -> std::mt19937& {
|
||||||
|
static std::mt19937 k_rng_{0xCAFEU};
|
||||||
|
return k_rng_;
|
||||||
|
}
|
||||||
|
|
||||||
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
||||||
std::vector<int> frames = {0};
|
std::vector<int> frames = {0};
|
||||||
float fps = 1.0F;
|
float fps = 1.0F;
|
||||||
@@ -56,6 +69,20 @@ Sarcophagus::Sarcophagus(const fkyaml::node& yaml)
|
|||||||
interactable_ = true;
|
interactable_ = true;
|
||||||
readTileSize(yaml, tile_size_w_, tile_size_h_);
|
readTileSize(yaml, tile_size_w_, tile_size_h_);
|
||||||
anim_ = anim_closed_;
|
anim_ = anim_closed_;
|
||||||
|
|
||||||
|
// Sons opcionals (bloc `sound:` del YAML). El basename val tant de
|
||||||
|
// clau al Resource::Cache com de path dins `Defaults::Paths::AUDIO_SOUNDS_DIR`.
|
||||||
|
// Mateix patró que `mummy.hurt.sound` i `player.jump.sound`.
|
||||||
|
if (yaml.contains("sound")) {
|
||||||
|
const auto& s = yaml["sound"];
|
||||||
|
if (s.contains("opening")) {
|
||||||
|
opening_sound_key_ = s["opening"].get_value<std::string>();
|
||||||
|
Resource::Cache::get().loadSound(opening_sound_key_, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + opening_sound_key_);
|
||||||
|
}
|
||||||
|
if (s.contains("opening_pitch_spread")) {
|
||||||
|
opening_pitch_spread_ = s["opening_pitch_spread"].get_value<float>();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Sarcophagus::place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked) {
|
void Sarcophagus::place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked) {
|
||||||
@@ -121,6 +148,30 @@ void Sarcophagus::notifyTilePainted(int col, int row) {
|
|||||||
state_ = State::OPENING;
|
state_ = State::OPENING;
|
||||||
anim_ = anim_opening_;
|
anim_ = anim_opening_;
|
||||||
if (anim_.has_value()) { anim_->reset(); }
|
if (anim_.has_value()) { anim_->reset(); }
|
||||||
|
if (!opening_sound_key_.empty() && Audio::get() != nullptr) {
|
||||||
|
// Variació de pitch/velocitat per evitar la fatiga d'escoltar
|
||||||
|
// el mateix sample idèntic a cada obertura. Si l'spread és 0
|
||||||
|
// (default), reproduïm amb ratio 1.0; en cas contrari sortegem
|
||||||
|
// uniformement [1 - spread, 1 + spread].
|
||||||
|
if (opening_pitch_spread_ > 0.0F) {
|
||||||
|
std::uniform_real_distribution<float> dist(1.0F - opening_pitch_spread_, 1.0F + opening_pitch_spread_);
|
||||||
|
Audio::get()->playSound(opening_sound_key_, Audio::Group::GAME, dist(rng()));
|
||||||
|
} else {
|
||||||
|
Audio::get()->playSound(opening_sound_key_, Audio::Group::GAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Sarcophagus::onRemoved() {
|
||||||
|
// El manager comptabilitza els sarcòfags actius en `total_sarcs_` per
|
||||||
|
// saber quan ha d'obrir la porta automàticament. Quan el editor
|
||||||
|
// n'esborra un, cal informar-lo: sense aquesta crida, `total_sarcs_`
|
||||||
|
// quedaria descompassat i la porta no s'obriria mai. Substitueix el
|
||||||
|
// `getName() == "sarcophagus"` que el manager feia abans —
|
||||||
|
// polimorfisme net, no comparació de strings.
|
||||||
|
if (entity_manager_ != nullptr) {
|
||||||
|
entity_manager_->onSarcophagusRemoved();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
@@ -43,6 +44,7 @@ class Sarcophagus : public Interactable {
|
|||||||
|
|
||||||
void update(float delta_time) override;
|
void update(float delta_time) override;
|
||||||
void notifyTilePainted(int col, int row) override;
|
void notifyTilePainted(int col, int row) override;
|
||||||
|
void onRemoved() override;
|
||||||
|
|
||||||
[[nodiscard]] auto getState() const -> State { return state_; }
|
[[nodiscard]] auto getState() const -> State { return state_; }
|
||||||
[[nodiscard]] auto getPerimeterSize() const -> std::size_t { return perimeter_tiles_.size(); }
|
[[nodiscard]] auto getPerimeterSize() const -> std::size_t { return perimeter_tiles_.size(); }
|
||||||
@@ -82,5 +84,12 @@ class Sarcophagus : public Interactable {
|
|||||||
AnimationComponent anim_opening_;
|
AnimationComponent anim_opening_;
|
||||||
AnimationComponent anim_open_;
|
AnimationComponent anim_open_;
|
||||||
|
|
||||||
|
// Clau (basename del fitxer) al Resource::Cache del so que es dispara
|
||||||
|
// a la transició CLOSED → OPENING. Buit ⇒ no sona res.
|
||||||
|
std::string opening_sound_key_;
|
||||||
|
// Variació uniforme del ratio de reproducció al voltant de 1.0. Cada
|
||||||
|
// obertura sorteja [1 - spread, 1 + spread]. 0 ⇒ ratio fix a 1.0.
|
||||||
|
float opening_pitch_spread_{0.0F};
|
||||||
|
|
||||||
bool opened_event_emitted_{false};
|
bool opened_event_emitted_{false};
|
||||||
};
|
};
|
||||||
@@ -262,19 +262,10 @@ void WhipAttack::update(float delta_time) {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
auto whipDirToMummyDir(WhipAttack::Direction d) -> Mummy::Direction {
|
// (El mapping WhipAttack::Direction → Mummy::Direction viu ara a
|
||||||
switch (d) {
|
// mummy.cpp, on s'usa des de `Mummy::onWhipHit`. Aquí només queda el
|
||||||
case WhipAttack::Direction::NORTH:
|
// mapping a Player::Cardinal — usat per al friendly-fire entre
|
||||||
return Mummy::Direction::NORTH;
|
// jugadors, que no és polimòrfic via Entity.)
|
||||||
case WhipAttack::Direction::SOUTH:
|
|
||||||
return Mummy::Direction::SOUTH;
|
|
||||||
case WhipAttack::Direction::EAST:
|
|
||||||
return Mummy::Direction::EAST;
|
|
||||||
case WhipAttack::Direction::WEST:
|
|
||||||
return Mummy::Direction::WEST;
|
|
||||||
}
|
|
||||||
return Mummy::Direction::NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto whipDirToPlayerCardinal(WhipAttack::Direction d) -> Player::Cardinal {
|
auto whipDirToPlayerCardinal(WhipAttack::Direction d) -> Player::Cardinal {
|
||||||
switch (d) {
|
switch (d) {
|
||||||
@@ -296,25 +287,24 @@ namespace {
|
|||||||
return STATE != Player::State::HURT && STATE != Player::State::EXITED && STATE != Player::State::DISABLED;
|
return STATE != Player::State::HURT && STATE != Player::State::EXITED && STATE != Player::State::DISABLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tracta una mòmia candidata: si és whippable i la seua caja solapa
|
// Tracta una entitat candidata: si la seva caixa solapa amb
|
||||||
// amb `damage_rect`, la posa a HURT i la marca al set. Extret per
|
// `damage_rect`, delega la decisió a `entity->onWhipHit(attack)`.
|
||||||
// acotar la complexitat cognitiva d'`applyDamage` (clang-tidy llindar).
|
// Cada entitat decideix què fer (Mummy entra HURT, Torch retorna
|
||||||
// Rep un `DynamicHazard*` (l'iterador d'`applyDamage` ja filtra el
|
// false sense efecte, RollingRock no override → no-op). El retorn
|
||||||
// sac d'actors); només cal el dynamic_cast a Mummy per descartar
|
// true marca el set `already_hit` per evitar segons impactes dins del
|
||||||
// altres hermanes (RollingRock i futurs DynamicHazards no whippables).
|
// mateix swing. Polimorfisme net — sense dynamic_cast.
|
||||||
void hitMummyIfApplicable(
|
void hitEntityIfApplicable(
|
||||||
DynamicHazard* actor,
|
Entity* entity,
|
||||||
const SDL_FRect& damage_rect,
|
const SDL_FRect& damage_rect,
|
||||||
WhipAttack::Direction dir,
|
WhipAttack* attack,
|
||||||
std::unordered_set<void*>& already_hit) {
|
std::unordered_set<void*>& already_hit) {
|
||||||
if (actor == nullptr || actor->isDead()) { return; }
|
if (entity == nullptr || entity->isDead()) { return; }
|
||||||
if (already_hit.contains(static_cast<void*>(actor))) { return; }
|
if (already_hit.contains(static_cast<void*>(entity))) { return; }
|
||||||
auto* mummy = dynamic_cast<Mummy*>(actor);
|
const auto BOX = entity->getCollisionRect();
|
||||||
if (mummy == nullptr || !mummy->isWhippable()) { return; }
|
|
||||||
const auto BOX = mummy->getCollisionRect();
|
|
||||||
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
|
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
|
||||||
mummy->enterHurt(whipDirToMummyDir(dir));
|
if (entity->onWhipHit(attack)) {
|
||||||
already_hit.insert(static_cast<void*>(actor));
|
already_hit.insert(static_cast<void*>(entity));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void hitPlayerIfApplicable(
|
void hitPlayerIfApplicable(
|
||||||
@@ -339,12 +329,14 @@ namespace {
|
|||||||
|
|
||||||
void WhipAttack::applyDamage() {
|
void WhipAttack::applyDamage() {
|
||||||
if (entity_manager_ != nullptr) {
|
if (entity_manager_ != nullptr) {
|
||||||
// Només els actors poden rebre el fuetejo (decoracions, sarcs,
|
// Iterem totes les entitats. La discriminació la fa cada una via
|
||||||
// forats... no són hits del whip). Iterem el sac `actors_`
|
// `onWhipHit`: Mummy entra HURT si és whippable, la resta retorna
|
||||||
// directament — sense recórrer tot el mapa per descartar-ne la
|
// false (no-op). El cost d'iterar antorxes/sarcs/forats és una
|
||||||
// major part.
|
// crida virtual buida cada una — menyspreable comparat amb la
|
||||||
for (auto* actor : entity_manager_->getActors()) {
|
// claredat del callsite. Si demà un sarc vol trencar-se al
|
||||||
hitMummyIfApplicable(actor, damage_rect_, direction_, already_hit_);
|
// fueteig, només cal override de `onWhipHit` allà.
|
||||||
|
for (auto* entity : entity_manager_->getEntities()) {
|
||||||
|
hitEntityIfApplicable(entity, damage_rect_, this, already_hit_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (players_ != nullptr) {
|
if (players_ != nullptr) {
|
||||||
|
|||||||
@@ -28,6 +28,31 @@ void Entity::notifyTilePainted(int /*col*/, int /*row*/) {
|
|||||||
// No-op per defecte; només `Sarcophagus` l'usa.
|
// No-op per defecte; només `Sarcophagus` l'usa.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto Entity::onWhipHit(WhipAttack* /*attack*/) -> bool {
|
||||||
|
// No-op per defecte; les entitats no-whippables (Torch, Hole, Sarc,
|
||||||
|
// Door, RollingRock...) retornen `false`. `Mummy` n'és l'override
|
||||||
|
// efectiu. El caller usa el retorn per marcar `already_hit_` —
|
||||||
|
// entitats que no han rebut dany NO bloquegen futurs frames del whip.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Entity::onJustSpawned() {
|
||||||
|
// No-op per defecte. `DynamicHazard` override per disparar el seu
|
||||||
|
// `triggerSpawnExit` (anima la fase EMERGING just després de néixer).
|
||||||
|
}
|
||||||
|
|
||||||
|
void Entity::onRemoved() {
|
||||||
|
// No-op per defecte. `Sarcophagus` override per decrementar el
|
||||||
|
// comptador `total_sarcs_` del manager.
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Entity::shouldReceiveContact() const -> bool {
|
||||||
|
// Default: l'entitat rep contacte si és sòlida (`collides_`) o si és
|
||||||
|
// un trigger d'àrea (`triggers_on_overlap_`). `DynamicHazard` afegeix
|
||||||
|
// el filtre de SPAWN — mentre està emergint, ignora contactes.
|
||||||
|
return collides_ || triggers_on_overlap_;
|
||||||
|
}
|
||||||
|
|
||||||
auto Entity::getCollisionRect() const -> SDL_FRect {
|
auto Entity::getCollisionRect() const -> SDL_FRect {
|
||||||
if (!collision_box_.has_value()) { return pixel_rect_; }
|
if (!collision_box_.has_value()) { return pixel_rect_; }
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class Player;
|
|||||||
class EventBus;
|
class EventBus;
|
||||||
class Surface;
|
class Surface;
|
||||||
class EntityManager;
|
class EntityManager;
|
||||||
|
class WhipAttack;
|
||||||
|
|
||||||
// Entitat genèrica del món. Actua com a dada en comú per a tots els actors
|
// Entitat genèrica del món. Actua com a dada en comú per a tots els actors
|
||||||
// del joc (decoratius, interactables, perills mòbils i estàtics): manté
|
// del joc (decoratius, interactables, perills mòbils i estàtics): manté
|
||||||
@@ -86,6 +87,38 @@ class Entity {
|
|||||||
// (per marcar perímetre); per defecte no-op.
|
// (per marcar perímetre); per defecte no-op.
|
||||||
virtual void notifyTilePainted(int col, int row);
|
virtual void notifyTilePainted(int col, int row);
|
||||||
|
|
||||||
|
// L'entitat ha rebut un fueteig amb solapament. Es passa el punter al
|
||||||
|
// `WhipAttack` perquè la subclasse pugui llegir-ne direcció o atributs
|
||||||
|
// (mateix patró que `onPlayerContact(Player*)`). Retorna `true` si
|
||||||
|
// efectivament ha aplicat dany — el caller en marca el set
|
||||||
|
// `already_hit_` per evitar segons impactes dins del mateix swing.
|
||||||
|
// Default: no fa res, retorna `false`. Override a `Mummy` (whippable)
|
||||||
|
// i a futurs enemics whippables. `RollingRock` no override → roca
|
||||||
|
// immune al látigo per defecte.
|
||||||
|
virtual auto onWhipHit(WhipAttack* attack) -> bool;
|
||||||
|
|
||||||
|
// Hook just després de néixer al món. El manager el crida quan
|
||||||
|
// `addSpawn` / `spawnActor` / `spawnPendingActors` instancien una
|
||||||
|
// entitat. La base no fa res; `DynamicHazard` override per disparar
|
||||||
|
// el seu `triggerSpawnExit` (anima la fase EMERGING). Substitueix el
|
||||||
|
// `dynamic_cast<DynamicHazard*>(observer)->triggerSpawnExit()` que el
|
||||||
|
// manager feia abans — ara el dispatch és polimòrfic.
|
||||||
|
virtual void onJustSpawned();
|
||||||
|
|
||||||
|
// Hook just abans que el manager esborri l'entitat (via
|
||||||
|
// `removeEntity`, típicament al editor). Permet a subclasses
|
||||||
|
// notificar al manager o publicar events per mantenir invariants
|
||||||
|
// (p.ex. `Sarcophagus` decrementa `total_sarcs_`). Default no-op.
|
||||||
|
virtual void onRemoved();
|
||||||
|
|
||||||
|
// Decisió polimòrfica de si aquesta entitat ha de rebre el callback
|
||||||
|
// `onPlayerContact` aquest frame. La base permet contacte si `collides_`
|
||||||
|
// o `triggers_on_overlap_` estan posats. Subclasses poden afegir
|
||||||
|
// condicions transitòries (p.ex. `DynamicHazard` retorna `false` mentre
|
||||||
|
// està en SPAWN). Centralitza el filtre que abans vivia al manager
|
||||||
|
// amb dynamic_casts/isSpawning explícits.
|
||||||
|
[[nodiscard]] virtual auto shouldReceiveContact() const -> bool;
|
||||||
|
|
||||||
[[nodiscard]] auto getName() const -> const std::string& { return name_; }
|
[[nodiscard]] auto getName() const -> const std::string& { return name_; }
|
||||||
[[nodiscard]] auto getTilePos() const -> TilePos { return tile_; }
|
[[nodiscard]] auto getTilePos() const -> TilePos { return tile_; }
|
||||||
[[nodiscard]] auto getPixelRect() const -> SDL_FRect { return pixel_rect_; }
|
[[nodiscard]] auto getPixelRect() const -> SDL_FRect { return pixel_rect_; }
|
||||||
|
|||||||
@@ -74,19 +74,7 @@ auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
|||||||
entity->setEventBus(&bus_);
|
entity->setEventBus(&bus_);
|
||||||
entity->setEntityManager(this);
|
entity->setEntityManager(this);
|
||||||
Entity* raw = entity.get();
|
Entity* raw = entity.get();
|
||||||
// Enrutament per categoria semàntica: si l'entitat és (o deriva de)
|
entities_.push_back(std::move(entity));
|
||||||
// DynamicHazard va al sac `actors_`, la resta al sac `map_entities_`.
|
|
||||||
// Així els consumidors que només volen actors itera el sac petit i
|
|
||||||
// ens estalviem dynamic_casts dispersos al codi (colisió, whip...).
|
|
||||||
if (dynamic_cast<DynamicHazard*>(entity.get()) != nullptr) {
|
|
||||||
// Transferim l'ownership al unique_ptr<DynamicHazard> tipat fort.
|
|
||||||
// El `release()` consumeix el seu valor com a argument del ctor —
|
|
||||||
// sense crida nua que el linter pugui marcar com a ignorada.
|
|
||||||
std::unique_ptr<DynamicHazard> hazard{static_cast<DynamicHazard*>(entity.release())};
|
|
||||||
actors_.push_back(std::move(hazard));
|
|
||||||
} else {
|
|
||||||
map_entities_.push_back(std::move(entity));
|
|
||||||
}
|
|
||||||
// cppcheck-suppress returnDanglingLifetime
|
// cppcheck-suppress returnDanglingLifetime
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
@@ -102,18 +90,15 @@ void EntityManager::setDoor(Door* door) {
|
|||||||
auto EntityManager::removeEntity(Entity* observer) -> bool {
|
auto EntityManager::removeEntity(Entity* observer) -> bool {
|
||||||
if (observer == nullptr) { return false; }
|
if (observer == nullptr) { return false; }
|
||||||
const bool WAS_DOOR = (door_ == observer);
|
const bool WAS_DOOR = (door_ == observer);
|
||||||
const bool WAS_SARC = (observer->getName() == "sarcophagus");
|
// `onRemoved` és polimòrfic: cada subclasse decideix què cal
|
||||||
// Busquem en els dos sacs. El sarc i la door sempre viuen a
|
// notificar quan se l'esborra. Avui només Sarcophagus l'override per
|
||||||
// `map_entities_` per disseny; un actor (DynamicHazard) viu a `actors_`.
|
// decrementar el seu comptador. La door es detecta per comparació de
|
||||||
if (auto it = std::ranges::find_if(map_entities_, [observer](const auto& up) { return up.get() == observer; }); it != map_entities_.end()) {
|
// punter (és única, no cal RTTI).
|
||||||
map_entities_.erase(it);
|
observer->onRemoved();
|
||||||
} else if (auto it_a = std::ranges::find_if(actors_, [observer](const auto& up) { return up.get() == observer; }); it_a != actors_.end()) {
|
auto it = std::ranges::find_if(entities_, [observer](const auto& up) { return up.get() == observer; });
|
||||||
actors_.erase(it_a);
|
if (it == entities_.end()) { return false; }
|
||||||
} else {
|
entities_.erase(it);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (WAS_DOOR) { door_ = nullptr; }
|
if (WAS_DOOR) { door_ = nullptr; }
|
||||||
if (WAS_SARC && total_sarcs_ > 0) { --total_sarcs_; }
|
|
||||||
// No tocar `opened_sarcs_` aquí: l'editor congela el joc i no pot
|
// No tocar `opened_sarcs_` aquí: l'editor congela el joc i no pot
|
||||||
// disparar-se cap esdeveniment derivat. Quan el GameScene faça el
|
// disparar-se cap esdeveniment derivat. Quan el GameScene faça el
|
||||||
// reload posterior, els comptadors es re-deriven d'inici.
|
// reload posterior, els comptadors es re-deriven d'inici.
|
||||||
@@ -125,8 +110,7 @@ auto EntityManager::addSarcophagus(std::unique_ptr<Sarcophagus> sarc) -> Sarcoph
|
|||||||
sarc->setEventBus(&bus_);
|
sarc->setEventBus(&bus_);
|
||||||
sarc->setEntityManager(this);
|
sarc->setEntityManager(this);
|
||||||
Sarcophagus* raw = sarc.get();
|
Sarcophagus* raw = sarc.get();
|
||||||
// El sarc és Interactable, viu al sac `map_entities_`.
|
entities_.push_back(std::move(sarc));
|
||||||
map_entities_.push_back(std::move(sarc));
|
|
||||||
++total_sarcs_;
|
++total_sarcs_;
|
||||||
// cppcheck-suppress returnDanglingLifetime
|
// cppcheck-suppress returnDanglingLifetime
|
||||||
return raw;
|
return raw;
|
||||||
@@ -134,34 +118,32 @@ auto EntityManager::addSarcophagus(std::unique_ptr<Sarcophagus> sarc) -> Sarcoph
|
|||||||
|
|
||||||
void EntityManager::update(float delta_time) {
|
void EntityManager::update(float delta_time) {
|
||||||
// ATENCIÓ: si en el futur una entitat publica al bus events que
|
// ATENCIÓ: si en el futur una entitat publica al bus events que
|
||||||
// resulten en `entity_manager_.add()` (push_back als sacs), açò
|
// resulten en `entity_manager_.add()` (push_back a `entities_`), açò
|
||||||
// invalidaria l'iterador de la ranged-for. Avui no passa: els spawns
|
// invalidaria l'iterador de la ranged-for. Avui no passa: els spawns
|
||||||
// runtime venen de fora del cicle d'`update` (p.ex. el polvet del
|
// runtime venen de fora del cicle d'`update` (p.ex. el polvet del
|
||||||
// saltador es spawneja durant `Player::tick`, abans d'aquest mètode).
|
// saltador es spawneja durant `Player::tick`, abans d'aquest mètode).
|
||||||
for (auto& e : map_entities_) { e->update(delta_time); }
|
for (auto& e : entities_) { e->update(delta_time); }
|
||||||
for (auto& a : actors_) { a->update(delta_time); }
|
|
||||||
// Purga d'entitats marcades mortes aquest frame (efectes acabats,
|
// Purga d'entitats marcades mortes aquest frame (efectes acabats,
|
||||||
// p.ex. polvet d'aterratge). Les persistents (sarcs, momies, etc.)
|
// p.ex. polvet d'aterratge). Les persistents (sarcs, momies, etc.)
|
||||||
// mai no es marquen.
|
// mai no es marquen.
|
||||||
std::erase_if(map_entities_, [](const auto& e) { return e->isDead(); });
|
std::erase_if(entities_, [](const auto& e) { return e->isDead(); });
|
||||||
std::erase_if(actors_, [](const auto& a) { return a->isDead(); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityManager::notifyPlayerTilePainted(int col, int row) {
|
void EntityManager::notifyPlayerTilePainted(int col, int row) {
|
||||||
// Fan-out als dos sacs. Avui només Sarcophagus consumeix (viu a
|
// Fan-out polimòrfic a totes les entitats: cada una decideix què fer
|
||||||
// map_entities_), però els actors també reben la notificació per
|
// (`Sarcophagus` actualitza el seu perímetre, la resta queden en
|
||||||
// si en el futur volen reaccionar (cost: una crida virtual buida).
|
// no-op). Cost d'iterar 40 elements amb crida virtual buida ~ 1 µs;
|
||||||
for (auto& e : map_entities_) { e->notifyTilePainted(col, row); }
|
// qui paga el cost guanya simplicitat al callsite.
|
||||||
for (auto& a : actors_) { a->notifyTilePainted(col, row); }
|
for (auto& e : entities_) { e->notifyTilePainted(col, row); }
|
||||||
}
|
}
|
||||||
|
|
||||||
auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* {
|
auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* {
|
||||||
// Cerca lineal sobre `map_entities_`: els floor decals trigger són
|
// Cerca lineal: només `Hole` (i futurs decals trigger) compleixen la
|
||||||
// exclusivament StaticHazards (Hole; en el futur pinchos plans,
|
// doble condició `triggersOnOverlap` + `isFloorDecal`. Costos típics
|
||||||
// plaques, charcos). Els actors no qualifiquen com a decals — saltem
|
// (poques desenes d'entitats) menyspreables, una crida per frame des
|
||||||
// directament aquest sac, sense `triggersOnOverlap`/`isFloorDecal` per
|
// dels consumidors actius (Player a l'aterratge, Mòmia a la decisió
|
||||||
// a cada un. La doble condició filtra dins de map_entities_.
|
// de direcció).
|
||||||
for (const auto& entity : map_entities_) {
|
for (const auto& entity : entities_) {
|
||||||
if (!entity->triggersOnOverlap()) { continue; }
|
if (!entity->triggersOnOverlap()) { continue; }
|
||||||
if (!entity->isFloorDecal()) { continue; }
|
if (!entity->isFloorDecal()) { continue; }
|
||||||
const auto BOX = entity->getCollisionRect();
|
const auto BOX = entity->getCollisionRect();
|
||||||
@@ -172,27 +154,9 @@ auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto EntityManager::getEntities() const -> std::vector<Entity*> {
|
auto EntityManager::getEntities() const -> std::vector<Entity*> {
|
||||||
// Concatenació map + actors per retro-compat (editor, z-sort debug).
|
|
||||||
// Els callers que ja saben quin sac volen poden cridar
|
|
||||||
// `getMapEntities` o `getActors` i estalviar la construcció del temp.
|
|
||||||
std::vector<Entity*> out;
|
std::vector<Entity*> out;
|
||||||
out.reserve(map_entities_.size() + actors_.size());
|
out.reserve(entities_.size());
|
||||||
std::ranges::transform(map_entities_, std::back_inserter(out), [](const auto& e) { return e.get(); });
|
std::ranges::transform(entities_, std::back_inserter(out), [](const auto& e) { return e.get(); });
|
||||||
std::ranges::transform(actors_, std::back_inserter(out), [](const auto& a) -> Entity* { return a.get(); });
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto EntityManager::getMapEntities() const -> std::vector<Entity*> {
|
|
||||||
std::vector<Entity*> out;
|
|
||||||
out.reserve(map_entities_.size());
|
|
||||||
std::ranges::transform(map_entities_, std::back_inserter(out), [](const auto& e) { return e.get(); });
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto EntityManager::getActors() const -> std::vector<DynamicHazard*> {
|
|
||||||
std::vector<DynamicHazard*> out;
|
|
||||||
out.reserve(actors_.size());
|
|
||||||
std::ranges::transform(actors_, std::back_inserter(out), [](const auto& a) { return a.get(); });
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
#include "game/entities/hazard.hpp"
|
|
||||||
|
|
||||||
class Entity;
|
class Entity;
|
||||||
class Sarcophagus;
|
class Sarcophagus;
|
||||||
class Door;
|
class Door;
|
||||||
class Player;
|
class Player;
|
||||||
|
class DynamicHazard;
|
||||||
|
|
||||||
// Contenidor i coordinador d'entitats d'un nivell. Responsabilitats:
|
// Contenidor i coordinador d'entitats d'un nivell. Responsabilitats:
|
||||||
// 1. `add` registra una entitat, la connecta al bus i manté propietat.
|
// 1. `add` registra una entitat, la connecta al bus i manté propietat.
|
||||||
@@ -74,14 +74,12 @@ class EntityManager {
|
|||||||
static void renderEntity(const Entity& entity);
|
static void renderEntity(const Entity& entity);
|
||||||
|
|
||||||
// Accés als punters observacionals a les entitats per a consumidors
|
// Accés als punters observacionals a les entitats per a consumidors
|
||||||
// externs que necessiten construir una llista ordenada (z-order).
|
// externs que necessiten iterar (z-order, aplicar dany, etc.). El
|
||||||
// `getEntities()` concatena els dos sacs interns (mapa + actors) per
|
// polimorfisme dins de cada entitat fa la discriminació: l'iterador
|
||||||
// a consumidors retro-compat (editor, z-sort debug). `getMapEntities`
|
// recorre tot i les entitats que no hagin de respondre tornen no-op
|
||||||
// i `getActors` exposen cada sac per separat — útil per a iteracions
|
// (p.ex. Torch::onWhipHit retorna false sense efecte). Sense sacs
|
||||||
// que ja saben quin tipus volen i estalvien filtres O(n).
|
// separats — el `if (entity_is_X)` viu dins de l'entitat, no del caller.
|
||||||
[[nodiscard]] auto getEntities() const -> std::vector<Entity*>;
|
[[nodiscard]] auto getEntities() const -> std::vector<Entity*>;
|
||||||
[[nodiscard]] auto getMapEntities() const -> std::vector<Entity*>;
|
|
||||||
[[nodiscard]] auto getActors() const -> std::vector<DynamicHazard*>;
|
|
||||||
|
|
||||||
// Comprova contacte del(s) jugador(s) amb entitats `collides()=true`.
|
// Comprova contacte del(s) jugador(s) amb entitats `collides()=true`.
|
||||||
// Si la caixa base del jugador solapa amb la caixa de col·lisió de
|
// Si la caixa base del jugador solapa amb la caixa de col·lisió de
|
||||||
@@ -160,7 +158,7 @@ class EntityManager {
|
|||||||
void setExpectedPlayerCount(int n);
|
void setExpectedPlayerCount(int n);
|
||||||
|
|
||||||
[[nodiscard]] auto getBus() -> EventBus& { return bus_; }
|
[[nodiscard]] auto getBus() -> EventBus& { return bus_; }
|
||||||
[[nodiscard]] auto entityCount() const -> std::size_t { return map_entities_.size() + actors_.size(); }
|
[[nodiscard]] auto entityCount() const -> std::size_t { return entities_.size(); }
|
||||||
[[nodiscard]] auto pendingActorCount() const -> std::size_t { return pending_actors_.size(); }
|
[[nodiscard]] auto pendingActorCount() const -> std::size_t { return pending_actors_.size(); }
|
||||||
[[nodiscard]] auto sarcophagusCount() const -> std::size_t { return total_sarcs_; }
|
[[nodiscard]] auto sarcophagusCount() const -> std::size_t { return total_sarcs_; }
|
||||||
[[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; }
|
[[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; }
|
||||||
@@ -173,19 +171,24 @@ class EntityManager {
|
|||||||
// sarcs. No-op si la porta ja està oberta o si el nivell no en té.
|
// sarcs. No-op si la porta ja està oberta o si el nivell no en té.
|
||||||
void forceOpenDoor();
|
void forceOpenDoor();
|
||||||
|
|
||||||
|
// Hook que `Sarcophagus::onRemoved` crida per decrementar el
|
||||||
|
// comptador `total_sarcs_`. Públic perquè el sarcòfag és la única
|
||||||
|
// entitat que necessita aquest manteniment, però l'API es manté
|
||||||
|
// genèrica: si en el futur Door volgués notificar quelcom semblant,
|
||||||
|
// afegim el seu propi callback.
|
||||||
|
void onSarcophagusRemoved() {
|
||||||
|
if (total_sarcs_ > 0) { --total_sarcs_; }
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
EventBus bus_;
|
EventBus bus_;
|
||||||
// Dos sacs lògics:
|
// Un sol contenidor lògic per a totes les entitats instanciades. La
|
||||||
// `map_entities_` — decoració, static_hazards (Hole, FloorTrap,
|
// discriminació de tipus la fan els virtuals (`onPlayerContact`,
|
||||||
// WallTrap) i interactables (Sarcophagus, Door).
|
// `onWhipHit`, `shouldReceiveContact`, `notifyTilePainted`...) — el
|
||||||
// Cargades al loadFromLevel i persistents durant
|
// manager itera i cada entitat decideix què fa. Hi conviuen
|
||||||
// tota la sala.
|
// decoracions, static_hazards, interactables i dynamic_hazards
|
||||||
// `actors_` — DynamicHazards (Mummy, RollingRock, futurs
|
// (actors).
|
||||||
// enemics). Separats per eliminar dynamic_casts
|
std::vector<std::unique_ptr<Entity>> entities_;
|
||||||
// i preparar el spawn diferit (commit posterior).
|
|
||||||
// `add()` enruta automàticament segons sigui o no DynamicHazard.
|
|
||||||
std::vector<std::unique_ptr<Entity>> map_entities_;
|
|
||||||
std::vector<std::unique_ptr<DynamicHazard>> actors_;
|
|
||||||
// Cua d'actors declarats al YAML del mapa però encara no instanciats:
|
// Cua d'actors declarats al YAML del mapa però encara no instanciats:
|
||||||
// `loadFromLevel` hi empeny tots els DynamicHazards. `spawnPendingActors`
|
// `loadFromLevel` hi empeny tots els DynamicHazards. `spawnPendingActors`
|
||||||
// o `spawnActor` consumeixen aquesta cua (o creen un actor on-demand)
|
// o `spawnActor` consumeixen aquesta cua (o creen un actor on-demand)
|
||||||
|
|||||||
@@ -57,11 +57,10 @@ void EntityManager::renderEntity(const Entity& e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EntityManager::renderAll() {
|
void EntityManager::renderAll() {
|
||||||
// Sense ordenació per z: l'ordre és el d'inserció dins de cada sac.
|
// Sense ordenació per z: l'ordre és el d'inserció al vector. El
|
||||||
// El render del joc real (GameScene) construeix el seu propi vector
|
// render del joc real (GameScene) construeix el seu propi vector
|
||||||
// ordenat per `feet_y`; aquest helper queda per al editor i tests.
|
// ordenat per `feet_y`; aquest helper queda per al editor i tests.
|
||||||
for (const auto& e : map_entities_) { renderEntity(*e); }
|
for (const auto& e : entities_) { renderEntity(*e); }
|
||||||
for (const auto& a : actors_) { renderEntity(*a); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -100,22 +99,15 @@ void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sac de mapa: filtre per col·lisió / trigger. Tanquem el gap latent
|
// Filtre polimòrfic unificat: cada entitat decideix si ha de rebre
|
||||||
// de FloorTrap/WallTrap (que tenien `collides_=false` i mai no es
|
// contacte (`shouldReceiveContact`). Per defecte equival a `collides()
|
||||||
// disparaven) i obrim la porta a hazards no-físics com `Hole`.
|
// || triggersOnOverlap()`; `DynamicHazard` afegeix el filtre de SPAWN
|
||||||
for (const auto& entity : map_entities_) {
|
// (mentre està emergint torna false). El callsite no fa dynamic_cast
|
||||||
if (!entity->collides() && !entity->triggersOnOverlap()) { continue; }
|
// ni distingeix categories.
|
||||||
|
for (const auto& entity : entities_) {
|
||||||
|
if (!entity->shouldReceiveContact()) { continue; }
|
||||||
apply_contacts(entity.get());
|
apply_contacts(entity.get());
|
||||||
}
|
}
|
||||||
// Sac d'actors (DynamicHazards): tots fan dany quan són actius.
|
|
||||||
// Saltem els que estan en SPAWN: visibles però sense interacció
|
|
||||||
// durant la fase inicial de ROOM_INTRO i durant futurs spawns en
|
|
||||||
// runtime. Sense dynamic_cast: el tipus ja és DynamicHazard.
|
|
||||||
for (const auto& actor : actors_) {
|
|
||||||
if (actor->isSpawning()) { continue; }
|
|
||||||
if (!actor->collides() && !actor->triggersOnOverlap()) { continue; }
|
|
||||||
apply_contacts(actor.get());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -413,8 +405,7 @@ void EntityManager::reload(const std::string& level_yaml_path) {
|
|||||||
// subscripcions implícites desapareixen amb la seua vida (cap d'elles
|
// subscripcions implícites desapareixen amb la seua vida (cap d'elles
|
||||||
// se subscriu directament al bus — només publiquen). El bus i les
|
// se subscriu directament al bus — només publiquen). El bus i les
|
||||||
// subscripcions externes (PaintLayer, HUD, GameScene) no es toquen.
|
// subscripcions externes (PaintLayer, HUD, GameScene) no es toquen.
|
||||||
map_entities_.clear();
|
entities_.clear();
|
||||||
actors_.clear();
|
|
||||||
pending_actors_.clear();
|
pending_actors_.clear();
|
||||||
door_ = nullptr;
|
door_ = nullptr;
|
||||||
total_sarcs_ = 0;
|
total_sarcs_ = 0;
|
||||||
@@ -478,9 +469,9 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
|||||||
// perquè tots els sarcòfags ja estan registrats al fan-out (la
|
// perquè tots els sarcòfags ja estan registrats al fan-out (la
|
||||||
// subscripció del manager és al ctor, anterior a qualsevol load).
|
// subscripció del manager és al ctor, anterior a qualsevol load).
|
||||||
int decal_broadcast = 0;
|
int decal_broadcast = 0;
|
||||||
// Els decals de sòl (Hole, futurs pinchos plans...) són StaticHazards
|
// Els decals de sòl (Hole, futurs pinchos plans...) són StaticHazards;
|
||||||
// i viuen exclusivament a `map_entities_`. Saltem `actors_` per estalviar.
|
// iterem tot `entities_` i el filtre per `isFloorDecal()` els tria.
|
||||||
for (const auto& entity : map_entities_) {
|
for (const auto& entity : entities_) {
|
||||||
if (!entity->isFloorDecal()) { continue; }
|
if (!entity->isFloorDecal()) { continue; }
|
||||||
const auto BOX = entity->getCollisionRect();
|
const auto BOX = entity->getCollisionRect();
|
||||||
const auto [COL, ROW] = Map::pixelToTile(BOX.x, BOX.y);
|
const auto [COL, ROW] = Map::pixelToTile(BOX.x, BOX.y);
|
||||||
@@ -495,22 +486,18 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
|||||||
Log::info("EntityManager", "{} tile_painted broadcast inicial (decals de sòl)", decal_broadcast);
|
Log::info("EntityManager", "{} tile_painted broadcast inicial (decals de sòl)", decal_broadcast);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log::info("EntityManager", "{} map entities + {} actors pending des de {}", loaded, pending_actors_.size(), level_yaml_path);
|
Log::info("EntityManager", "{} entitats + {} actors pending des de {}", loaded, pending_actors_.size(), level_yaml_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityManager::spawnPendingActors() {
|
void EntityManager::spawnPendingActors() {
|
||||||
// Itera els pending i els instancia via `addSpawn`. El dispatch d'`add()`
|
// Itera els pending i els instancia via `addSpawn`. El dispatch d'`add()`
|
||||||
// els enruta a `actors_`. El `triggerSpawnExit()` posterior arma el
|
// els enruta a `actors_`. `onJustSpawned()` és polimòrfic: la base
|
||||||
// comptador de SPAWN — la mòmia neix ja en EMERGING, sense esperes.
|
// no fa res, DynamicHazard l'override per disparar `triggerSpawnExit()`
|
||||||
|
// i entrar a EMERGING. Sense dynamic_cast: la decisió viu dins de
|
||||||
|
// cada entitat.
|
||||||
for (const auto& pa : pending_actors_) {
|
for (const auto& pa : pending_actors_) {
|
||||||
Entity* observer = addSpawn(pa.spawn_node);
|
Entity* observer = addSpawn(pa.spawn_node);
|
||||||
if (observer == nullptr) { continue; }
|
if (observer != nullptr) { observer->onJustSpawned(); }
|
||||||
// Per categoria YAML (`type: dynamic_hazard`) sabem que és
|
|
||||||
// DynamicHazard — el dynamic_cast aquí val zero i evita
|
|
||||||
// assumptions implícites sobre la jerarquia.
|
|
||||||
if (auto* dh = dynamic_cast<DynamicHazard*>(observer); dh != nullptr) {
|
|
||||||
dh->triggerSpawnExit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pending_actors_.clear();
|
pending_actors_.clear();
|
||||||
}
|
}
|
||||||
@@ -530,7 +517,9 @@ auto EntityManager::spawnActor(const std::string& type, int col, int row) -> Dyn
|
|||||||
}
|
}
|
||||||
Entity* observer = addSpawn(spawn);
|
Entity* observer = addSpawn(spawn);
|
||||||
if (observer == nullptr) { return nullptr; }
|
if (observer == nullptr) { return nullptr; }
|
||||||
auto* dh = dynamic_cast<DynamicHazard*>(observer);
|
// `onJustSpawned` polimòrfic: per a DynamicHazards arma EMERGING; per
|
||||||
if (dh != nullptr) { dh->triggerSpawnExit(); }
|
// a un type erroni (no dynamic_hazard) queda no-op i retornem nullptr
|
||||||
return dh;
|
// mitjançant el cast final, deixant la signatura `DynamicHazard*` neta.
|
||||||
|
observer->onJustSpawned();
|
||||||
|
return dynamic_cast<DynamicHazard*>(observer);
|
||||||
}
|
}
|
||||||
@@ -96,9 +96,28 @@ class DynamicHazard : public Hazard {
|
|||||||
// Arma el comptador del SPAWN. A partir d'aquí, cada `update(dt)`
|
// Arma el comptador del SPAWN. A partir d'aquí, cada `update(dt)`
|
||||||
// descompta `dt` fins arribar a 0 i transitar a ACTIVE.
|
// descompta `dt` fins arribar a 0 i transitar a ACTIVE.
|
||||||
void triggerSpawnExit() { spawn_exit_armed_ = true; }
|
void triggerSpawnExit() { spawn_exit_armed_ = true; }
|
||||||
|
// Transició directa a ACTIVE, sense passar pel countdown. Pensat
|
||||||
|
// per a subclasses amb SPAWN dirigit per animació: gestionen el
|
||||||
|
// seu propi `update()` mentre `isSpawning()` i criden ací quan la
|
||||||
|
// seua animació d'aparició ha completat.
|
||||||
|
void endSpawn() { spawn_state_ = SpawnState::ACTIVE; }
|
||||||
[[nodiscard]] auto isSpawning() const -> bool { return spawn_state_ == SpawnState::SPAWNING; }
|
[[nodiscard]] auto isSpawning() const -> bool { return spawn_state_ == SpawnState::SPAWNING; }
|
||||||
[[nodiscard]] auto getSpawnDurationS() const -> float { return spawn_duration_s_; }
|
[[nodiscard]] auto getSpawnDurationS() const -> float { return spawn_duration_s_; }
|
||||||
|
|
||||||
|
// Just després d'instanciar-se al món (via spawnPendingActors /
|
||||||
|
// spawnActor), l'actor entra automàticament en la fase EMERGING:
|
||||||
|
// arma el comptador i comença l'animació de sortida. Substitueix el
|
||||||
|
// dynamic_cast que el manager feia abans per cridar `triggerSpawnExit`.
|
||||||
|
void onJustSpawned() override { triggerSpawnExit(); }
|
||||||
|
|
||||||
|
// Mentre està en SPAWN (HIDDEN/EMERGING), l'actor NO rep contacte
|
||||||
|
// ni amb el jugador ni amb attacks: visible però "no juga". Quan
|
||||||
|
// passa a ACTIVE, recupera el comportament estàndard d'Entity.
|
||||||
|
[[nodiscard]] auto shouldReceiveContact() const -> bool override {
|
||||||
|
if (isSpawning()) { return false; }
|
||||||
|
return Entity::shouldReceiveContact();
|
||||||
|
}
|
||||||
|
|
||||||
// Política de moviment per frame. La base desplaça el pixel_rect_
|
// Política de moviment per frame. La base desplaça el pixel_rect_
|
||||||
// proporcionalment a velocitat i direcció; les subclasses amb IA la
|
// proporcionalment a velocitat i direcció; les subclasses amb IA la
|
||||||
// sobreescriuen per triar nova direcció a les interseccions o rebotar.
|
// sobreescriuen per triar nova direcció a les interseccions o rebotar.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "core/input/gamepad_manager.hpp"
|
#include "core/input/gamepad_manager.hpp"
|
||||||
#include "core/input/input_state.hpp"
|
#include "core/input/input_state.hpp"
|
||||||
#include "core/rendering/canvas.hpp"
|
#include "core/rendering/canvas.hpp"
|
||||||
|
#include "core/rendering/effects/shadow.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
#include "core/rendering/surface.hpp"
|
#include "core/rendering/surface.hpp"
|
||||||
#include "core/resources/palette_loader.hpp"
|
#include "core/resources/palette_loader.hpp"
|
||||||
@@ -179,7 +180,6 @@ void Player::readJumpBlock(const fkyaml::node& yaml) {
|
|||||||
if (j.contains("impulse")) { jump_config.impulse_px_per_s = j["impulse"].get_value<float>(); }
|
if (j.contains("impulse")) { jump_config.impulse_px_per_s = j["impulse"].get_value<float>(); }
|
||||||
if (j.contains("gravity")) { jump_config.gravity_px_per_s2 = j["gravity"].get_value<float>(); }
|
if (j.contains("gravity")) { jump_config.gravity_px_per_s2 = j["gravity"].get_value<float>(); }
|
||||||
if (j.contains("landing_stun_ms")) { jump_config.landing_stun_ms = j["landing_stun_ms"].get_value<int>(); }
|
if (j.contains("landing_stun_ms")) { jump_config.landing_stun_ms = j["landing_stun_ms"].get_value<int>(); }
|
||||||
if (j.contains("shadow_y_offset")) { jump_config.shadow_y_offset = j["shadow_y_offset"].get_value<int>(); }
|
|
||||||
// Sons opcionals: el nom del fitxer (relatiu a Defaults::Paths::AUDIO_SOUNDS_DIR)
|
// Sons opcionals: el nom del fitxer (relatiu a Defaults::Paths::AUDIO_SOUNDS_DIR)
|
||||||
// val tant de clau al Resource::Cache com de basename per a la càrrega.
|
// val tant de clau al Resource::Cache com de basename per a la càrrega.
|
||||||
// Si el camp manca, la clau queda buida i `playSound` no es crida.
|
// Si el camp manca, la clau queda buida i `playSound` no es crida.
|
||||||
@@ -1435,36 +1435,17 @@ void Player::renderShadow() const {
|
|||||||
// que `enterDying` netegi el flag.
|
// que `enterDying` netegi el flag.
|
||||||
if (falling_through_hole_) { return; }
|
if (falling_through_hole_) { return; }
|
||||||
|
|
||||||
// Sombra centrada al sprite, recolzada a la línia dels peus reals
|
// `height_factor`: 0 al terra → fotograma més gran, 1 al pic → més
|
||||||
// (ignorant `jump_z_`). L'amplada es redueix proporcional a l'altura
|
// petit. Si el `jump_config` té un divisor zero (cas degenerat) caem
|
||||||
// del salt — al pic queda més estreta, donant la sensació de
|
// a 0 per no propagar nan al renderer.
|
||||||
// "elevació" sense necessitat de coordenades 3D reals. Si el
|
|
||||||
// jump_config té un divisor zero (cas degenerat), només dibuixem
|
|
||||||
// la sombra a mida plena per evitar nan.
|
|
||||||
const auto PEAK = (jump_config.gravity_px_per_s2 > 0.0F)
|
const auto PEAK = (jump_config.gravity_px_per_s2 > 0.0F)
|
||||||
? (jump_config.impulse_px_per_s * jump_config.impulse_px_per_s) / (2.0F * jump_config.gravity_px_per_s2)
|
? (jump_config.impulse_px_per_s * jump_config.impulse_px_per_s) / (2.0F * jump_config.gravity_px_per_s2)
|
||||||
: 0.0F;
|
: 0.0F;
|
||||||
const float NORM_Z = (PEAK > 0.0F) ? std::clamp(jump_z_ / PEAK, 0.0F, 1.0F) : 0.0F;
|
const float HEIGHT_FACTOR = (PEAK > 0.0F) ? std::clamp(jump_z_ / PEAK, 0.0F, 1.0F) : 0.0F;
|
||||||
constexpr float MAX_SHRINK = 0.4F; // Fins a un 40% més estreta al pic.
|
|
||||||
const float SHRINK = 1.0F - (NORM_Z * MAX_SHRINK);
|
|
||||||
|
|
||||||
constexpr int SHADOW_H = 3;
|
const auto CENTER_X = static_cast<int>(getPosX() + (static_cast<float>(sprite_w) * 0.5F));
|
||||||
const auto SHADOW_W = static_cast<int>(static_cast<float>(base_size) * SHRINK);
|
const auto FEET_Y = static_cast<int>(getGroundFeetY());
|
||||||
const float CX = getPosX() + (static_cast<float>(sprite_w) * 0.5F);
|
Effects::Shadow::render(CENTER_X, FEET_Y, HEIGHT_FACTOR);
|
||||||
const float GROUND_Y = getGroundFeetY();
|
|
||||||
const auto SX = static_cast<int>(CX - (static_cast<float>(SHADOW_W) * 0.5F));
|
|
||||||
// Pugem la sombra `shadow_y_offset` px per compensar la franja
|
|
||||||
// transparent que el sprite del jugador té sota els peus reals
|
|
||||||
// (perquè la silueta camine pel mig del passadís en falsa
|
|
||||||
// perspectiva). Sense aquest ajust, la sombra apareix per sota
|
|
||||||
// del passadís.
|
|
||||||
const auto SY = static_cast<int>(GROUND_Y) - SHADOW_H - jump_config.shadow_y_offset;
|
|
||||||
|
|
||||||
// ARGB negre amb alpha ~40%. `blendRect` mescla sobre el canvas; així
|
|
||||||
// la sombra fosqueja el sòl (pintat o no) sense tapar-lo del tot.
|
|
||||||
constexpr std::uint32_t SHADOW_ARGB = 0x00000000U;
|
|
||||||
constexpr std::uint8_t SHADOW_ALPHA = 100;
|
|
||||||
Screen::get().blendRect(SX, SY, SHADOW_W, SHADOW_H, SHADOW_ARGB, SHADOW_ALPHA);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sufix d'animació per cardinal — empat 1:1 amb l'enum `Facing` i el
|
// Sufix d'animació per cardinal — empat 1:1 amb l'enum `Facing` i el
|
||||||
|
|||||||
@@ -633,7 +633,6 @@ class Player : public AnimatedSprite {
|
|||||||
float impulse_px_per_s{0.0F}; // Velocitat vertical inicial.
|
float impulse_px_per_s{0.0F}; // Velocitat vertical inicial.
|
||||||
float gravity_px_per_s2{0.0F}; // Acceleració descendent constant.
|
float gravity_px_per_s2{0.0F}; // Acceleració descendent constant.
|
||||||
int landing_stun_ms{0}; // Stun en aterrar (no es mou ni salta).
|
int landing_stun_ms{0}; // Stun en aterrar (no es mou ni salta).
|
||||||
int shadow_y_offset{0}; // Px que puja la sombra (compensa transparències al peu del sprite).
|
|
||||||
std::string sound_key; // Clau al Resource::Cache del so de despegue (buit = silenci).
|
std::string sound_key; // Clau al Resource::Cache del so de despegue (buit = silenci).
|
||||||
std::string land_sound_key; // Clau del so d'aterratge (buit = silenci).
|
std::string land_sound_key; // Clau del so d'aterratge (buit = silenci).
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1011,11 +1011,6 @@ void GameScene::renderRoomFade() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::renderRoomText() {
|
void GameScene::renderRoomText() {
|
||||||
// Orquestrador del rètol de transició de sala. La feina pesada
|
|
||||||
// (animació de X, animació d'altura del panel i pintat) la fan
|
|
||||||
// quatre helpers privats; aquesta funció només decideix quin
|
|
||||||
// text dibuixar i amb quines fases segons l'estat.
|
|
||||||
//
|
|
||||||
// Mapatge per estat (només els estats amb text actiu hi entren):
|
// Mapatge per estat (només els estats amb text actiu hi entren):
|
||||||
// ROOM_INTRO: phases 2..4 → enter / hold / exit; dues línies
|
// ROOM_INTRO: phases 2..4 → enter / hold / exit; dues línies
|
||||||
// (piràmide damunt, sala davall) que travessen la
|
// (piràmide damunt, sala davall) que travessen la
|
||||||
@@ -1024,82 +1019,35 @@ void GameScene::renderRoomText() {
|
|||||||
// oposades ("fase" / "completada"). Phase 3 és el
|
// oposades ("fase" / "completada"). Phase 3 és el
|
||||||
// fade-out i 4 la pausa en negre — cap text en cap
|
// fade-out i 4 la pausa en negre — cap text en cap
|
||||||
// de les dues.
|
// de les dues.
|
||||||
if (!room_text_font_) { return; }
|
|
||||||
|
|
||||||
constexpr int KERNING = 0;
|
|
||||||
// Centre vertical de la zona de joc (canvas sense la HUD). Reemplaça
|
|
||||||
// l'antic GameCanvas::CENTER_Y per descomptar la franja del marcador
|
|
||||||
// i tenir el rètol equilibrat sobre el playfield.
|
|
||||||
const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2;
|
|
||||||
// Colors del rètol via UiConfig (`data/config/ui.yaml > colors.room_text` i
|
|
||||||
// `room_text_outline`). Independents dels colors de notificació.
|
|
||||||
const Uint8 FILL = UiConfig::get().roomText();
|
|
||||||
const Uint8 OUTLINE = UiConfig::get().roomTextOutline();
|
|
||||||
const int LINE_HEIGHT = room_text_font_->getLineHeight();
|
|
||||||
|
|
||||||
const int PHASE = transition_.phase;
|
const int PHASE = transition_.phase;
|
||||||
const int ELAPSED = transition_.elapsed_ms;
|
const int ELAPSED = transition_.elapsed_ms;
|
||||||
if (state_ == RoomFsm::State::ROOM_INTRO && PHASE >= 2 && PHASE <= 4) {
|
if (state_ == RoomFsm::State::ROOM_INTRO && PHASE >= 2 && PHASE <= 4) {
|
||||||
const int DUR_ENTER = transition_config_.intro_text_enter_ms;
|
|
||||||
const int DUR_EXIT = transition_config_.intro_text_exit_ms;
|
|
||||||
// Panel primer (queda sota el text gràcies a l'ordre de blits).
|
|
||||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
|
||||||
computeRoomTextPanelHeight(PHASE, ELAPSED, 2, 3, 4, DUR_ENTER, DUR_EXIT));
|
|
||||||
|
|
||||||
const auto& mgr = Levels::LevelManager::get();
|
const auto& mgr = Levels::LevelManager::get();
|
||||||
const std::string LINE_PYRAMID = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid());
|
renderTextScreen(TextScreenSpec{
|
||||||
const std::string LINE_ROOM = RoomFsm::formatIntroRoomLine(mgr.currentRoom());
|
.line1 = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid()),
|
||||||
// Direccions oposades: piràmide entra per l'esquerra i surt per
|
.line2 = RoomFsm::formatIntroRoomLine(mgr.currentRoom()),
|
||||||
// la dreta; sala fa el contrari.
|
.phase_enter = 2,
|
||||||
const int X_PYRAMID = computeRoomTextX(room_text_font_->length(LINE_PYRAMID, KERNING),
|
.phase_hold = 3,
|
||||||
|
.phase_exit = 4,
|
||||||
|
.dur_enter_ms = transition_config_.intro_text_enter_ms,
|
||||||
|
.dur_exit_ms = transition_config_.intro_text_exit_ms,
|
||||||
|
.line_gap_px = transition_config_.intro_text_line_gap_px,
|
||||||
|
},
|
||||||
PHASE,
|
PHASE,
|
||||||
ELAPSED,
|
ELAPSED);
|
||||||
2,
|
|
||||||
4,
|
|
||||||
DUR_ENTER,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/false);
|
|
||||||
const int X_ROOM = computeRoomTextX(room_text_font_->length(LINE_ROOM, KERNING),
|
|
||||||
PHASE,
|
|
||||||
ELAPSED,
|
|
||||||
2,
|
|
||||||
4,
|
|
||||||
DUR_ENTER,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/true);
|
|
||||||
// Bloc de dues línies centrat al voltant del PLAY_CENTER_Y. El
|
|
||||||
// gap (`intro_text_line_gap_px`) es reparteix simètricament:
|
|
||||||
// mig amunt a la piràmide, mig avall a la sala.
|
|
||||||
const int GAP = transition_config_.intro_text_line_gap_px;
|
|
||||||
drawRoomTextLine(LINE_PYRAMID, X_PYRAMID, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
|
||||||
drawRoomTextLine(LINE_ROOM, X_ROOM, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
|
||||||
} else if (state_ == RoomFsm::State::ROOM_OUTRO && PHASE >= 0 && PHASE <= 2) {
|
} else if (state_ == RoomFsm::State::ROOM_OUTRO && PHASE >= 0 && PHASE <= 2) {
|
||||||
const int DUR_ENTER = transition_config_.outro_text_enter_ms;
|
renderTextScreen(TextScreenSpec{
|
||||||
const int DUR_EXIT = transition_config_.outro_text_exit_ms;
|
.line1 = Locale::get().text("game.room_complete_line1"),
|
||||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
.line2 = Locale::get().text("game.room_complete_line2"),
|
||||||
computeRoomTextPanelHeight(PHASE, ELAPSED, 0, 1, 2, DUR_ENTER, DUR_EXIT));
|
.phase_enter = 0,
|
||||||
|
.phase_hold = 1,
|
||||||
const std::string LINE_1 = Locale::get().text("game.room_complete_line1");
|
.phase_exit = 2,
|
||||||
const std::string LINE_2 = Locale::get().text("game.room_complete_line2");
|
.dur_enter_ms = transition_config_.outro_text_enter_ms,
|
||||||
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
.dur_exit_ms = transition_config_.outro_text_exit_ms,
|
||||||
|
.line_gap_px = transition_config_.outro_text_line_gap_px,
|
||||||
|
},
|
||||||
PHASE,
|
PHASE,
|
||||||
ELAPSED,
|
ELAPSED);
|
||||||
0,
|
|
||||||
2,
|
|
||||||
DUR_ENTER,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/false);
|
|
||||||
const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING),
|
|
||||||
PHASE,
|
|
||||||
ELAPSED,
|
|
||||||
0,
|
|
||||||
2,
|
|
||||||
DUR_ENTER,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/true);
|
|
||||||
const int GAP = transition_config_.outro_text_line_gap_px;
|
|
||||||
drawRoomTextLine(LINE_1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
|
||||||
drawRoomTextLine(LINE_2, X_2, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1179,6 +1127,51 @@ void GameScene::drawRoomTextLine(const std::string& text, int x, int y, std::uin
|
|||||||
outline_color);
|
outline_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameScene::renderTextScreen(const TextScreenSpec& spec, int phase, int elapsed_ms) const {
|
||||||
|
// Coreografia comuna als quatre rètols (ROOM_INTRO, ROOM_OUTRO,
|
||||||
|
// CONTINUE?, GAME OVER): panel translúcid de fons + dues línies
|
||||||
|
// que entren/surten per costats oposats. Els quatre helpers
|
||||||
|
// existents (`drawRoomTextPanel`, `computeRoomTextPanelHeight`,
|
||||||
|
// `computeRoomTextX`, `drawRoomTextLine`) fan la feina pesada;
|
||||||
|
// aquesta funció només els encadena en el mateix ordre per als
|
||||||
|
// quatre casos. Una `lineN` buida es salta sense pintar — així
|
||||||
|
// CONTINUE? amaga la línia del countdown fora del hold sense
|
||||||
|
// que els callers hagin de duplicar la branca.
|
||||||
|
if (!room_text_font_) { return; }
|
||||||
|
|
||||||
|
constexpr int KERNING = 0;
|
||||||
|
const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2;
|
||||||
|
const Uint8 FILL = UiConfig::get().roomText();
|
||||||
|
const Uint8 OUTLINE = UiConfig::get().roomTextOutline();
|
||||||
|
const int LINE_HEIGHT = room_text_font_->getLineHeight();
|
||||||
|
|
||||||
|
drawRoomTextPanel(PLAY_CENTER_Y,
|
||||||
|
computeRoomTextPanelHeight(phase, elapsed_ms, spec.phase_enter, spec.phase_hold, spec.phase_exit, spec.dur_enter_ms, spec.dur_exit_ms));
|
||||||
|
|
||||||
|
if (!spec.line1.empty()) {
|
||||||
|
const int X_1 = computeRoomTextX(room_text_font_->length(spec.line1, KERNING),
|
||||||
|
phase,
|
||||||
|
elapsed_ms,
|
||||||
|
spec.phase_enter,
|
||||||
|
spec.phase_exit,
|
||||||
|
spec.dur_enter_ms,
|
||||||
|
spec.dur_exit_ms,
|
||||||
|
/*from_right=*/false);
|
||||||
|
drawRoomTextLine(spec.line1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (spec.line_gap_px / 2), FILL, OUTLINE);
|
||||||
|
}
|
||||||
|
if (!spec.line2.empty()) {
|
||||||
|
const int X_2 = computeRoomTextX(room_text_font_->length(spec.line2, KERNING),
|
||||||
|
phase,
|
||||||
|
elapsed_ms,
|
||||||
|
spec.phase_enter,
|
||||||
|
spec.phase_exit,
|
||||||
|
spec.dur_enter_ms,
|
||||||
|
spec.dur_exit_ms,
|
||||||
|
/*from_right=*/true);
|
||||||
|
drawRoomTextLine(spec.line2, X_2, PLAY_CENTER_Y + (spec.line_gap_px / 2), FILL, OUTLINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameScene::setGameOverState(GameOverFsm::State new_state) {
|
void GameScene::setGameOverState(GameOverFsm::State new_state) {
|
||||||
if (new_state == game_over_state_) { return; }
|
if (new_state == game_over_state_) { return; }
|
||||||
const auto PREV = game_over_state_;
|
const auto PREV = game_over_state_;
|
||||||
@@ -1315,104 +1308,57 @@ void GameScene::emitCountdownTickIfChanged() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::renderGameOverText() {
|
void GameScene::renderGameOverText() {
|
||||||
if (!room_text_font_) { return; }
|
// Mapatge per estat:
|
||||||
|
// CONTINUE_PROMPT: phases 0=enter, 1=hold (countdown visible),
|
||||||
|
// 2=exit. La línia 2 (número) només es pinta
|
||||||
|
// durant la fase 1 perquè els números no
|
||||||
|
// travessin la pantalla.
|
||||||
|
// GAME_OVER: phases 0=hold, 1=exit (sense enter anim — el
|
||||||
|
// panel ja és amunt del CONTINUE_PROMPT). Phase 2
|
||||||
|
// (wait) sense text ni panel.
|
||||||
if (game_over_state_ != GameOverFsm::State::CONTINUE_PROMPT && game_over_state_ != GameOverFsm::State::GAME_OVER) {
|
if (game_over_state_ != GameOverFsm::State::CONTINUE_PROMPT && game_over_state_ != GameOverFsm::State::GAME_OVER) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr int KERNING = 0;
|
|
||||||
const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2;
|
|
||||||
const Uint8 FILL = UiConfig::get().roomText();
|
|
||||||
const Uint8 OUTLINE = UiConfig::get().roomTextOutline();
|
|
||||||
const int LINE_HEIGHT = room_text_font_->getLineHeight();
|
|
||||||
const int PHASE = game_over_transition_.phase;
|
const int PHASE = game_over_transition_.phase;
|
||||||
const int ELAPSED = game_over_transition_.elapsed_ms;
|
const int ELAPSED = game_over_transition_.elapsed_ms;
|
||||||
|
|
||||||
if (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT) {
|
if (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT) {
|
||||||
const int DUR_ENTER = game_over_config_.continue_text_enter_ms;
|
|
||||||
const int DUR_EXIT = game_over_config_.continue_text_exit_ms;
|
|
||||||
// Phases del CONTINUE_PROMPT: 0=enter, 1=hold, 2=exit. El panel
|
|
||||||
// segueix la mateixa corba enter/hold/exit que el room text.
|
|
||||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
|
||||||
computeRoomTextPanelHeight(PHASE, ELAPSED, 0, 1, 2, DUR_ENTER, DUR_EXIT));
|
|
||||||
|
|
||||||
const std::string LINE_1 = Locale::get().text("game.continue_prompt");
|
|
||||||
const int SECONDS = GameOverFsm::countdownSecondsRemaining(
|
const int SECONDS = GameOverFsm::countdownSecondsRemaining(
|
||||||
game_over_state_,
|
game_over_state_,
|
||||||
game_over_transition_,
|
game_over_transition_,
|
||||||
game_over_config_);
|
game_over_config_);
|
||||||
// Línia 2 només té text durant la fase de hold (countdown). En
|
renderTextScreen(TextScreenSpec{
|
||||||
// les fases d'enter i exit deixem només el "CONTINUE?" perquè
|
.line1 = Locale::get().text("game.continue_prompt"),
|
||||||
// l'animació dels números no quedi rara.
|
.line2 = (PHASE == 1) ? std::to_string(SECONDS) : std::string{},
|
||||||
const std::string LINE_2 = (PHASE == 1) ? std::to_string(SECONDS) : std::string{};
|
.phase_enter = 0,
|
||||||
|
.phase_hold = 1,
|
||||||
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
.phase_exit = 2,
|
||||||
|
.dur_enter_ms = game_over_config_.continue_text_enter_ms,
|
||||||
|
.dur_exit_ms = game_over_config_.continue_text_exit_ms,
|
||||||
|
.line_gap_px = transition_config_.intro_text_line_gap_px,
|
||||||
|
},
|
||||||
PHASE,
|
PHASE,
|
||||||
ELAPSED,
|
ELAPSED);
|
||||||
0,
|
|
||||||
2,
|
|
||||||
DUR_ENTER,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/false);
|
|
||||||
const int X_2 = computeRoomTextX(LINE_2.empty() ? 0 : room_text_font_->length(LINE_2, KERNING),
|
|
||||||
PHASE,
|
|
||||||
ELAPSED,
|
|
||||||
0,
|
|
||||||
2,
|
|
||||||
DUR_ENTER,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/true);
|
|
||||||
const int GAP = transition_config_.intro_text_line_gap_px;
|
|
||||||
drawRoomTextLine(LINE_1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
|
||||||
if (!LINE_2.empty()) {
|
|
||||||
drawRoomTextLine(LINE_2, X_2, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GAME_OVER: panel sempre amunt durant phase 0/1, decreix en phase 1
|
// GAME_OVER: phase 2 (wait) sense panel ni text — només temps mort
|
||||||
// (exit). Phase 2 (wait) sense panel ni text — només temps mort
|
// abans del fade. phase_enter=-1 desactiva l'animació d'entrada
|
||||||
// abans del fade.
|
// (el panel ja és amunt heretat del CONTINUE_PROMPT).
|
||||||
const int DUR_HOLD = game_over_config_.game_over_text_hold_ms;
|
|
||||||
const int DUR_EXIT = game_over_config_.game_over_text_exit_ms;
|
|
||||||
if (PHASE > 1) { return; }
|
if (PHASE > 1) { return; }
|
||||||
|
renderTextScreen(TextScreenSpec{
|
||||||
// Panel: phase 0 = ple, phase 1 = decreix. Reaprofitem
|
.line1 = Locale::get().text("game.game_over_line1"),
|
||||||
// computeRoomTextPanelHeight passant un phase_enter "fora de rang"
|
.line2 = Locale::get().text("game.game_over_line2"),
|
||||||
// (-1) perquè la branca d'enter no s'activi mai.
|
.phase_enter = -1,
|
||||||
drawRoomTextPanel(PLAY_CENTER_Y,
|
.phase_hold = 0,
|
||||||
computeRoomTextPanelHeight(PHASE, ELAPSED, /*phase_enter=*/-1,
|
.phase_exit = 1,
|
||||||
/*phase_hold=*/0,
|
.dur_enter_ms = 0,
|
||||||
/*phase_exit=*/1,
|
.dur_exit_ms = game_over_config_.game_over_text_exit_ms,
|
||||||
/*dur_enter=*/0,
|
.line_gap_px = transition_config_.outro_text_line_gap_px,
|
||||||
DUR_EXIT));
|
},
|
||||||
|
|
||||||
const std::string LINE_1 = Locale::get().text("game.game_over_line1");
|
|
||||||
const std::string LINE_2 = Locale::get().text("game.game_over_line2");
|
|
||||||
// Phase 0 (hold): text quiet al CENTER_X. Phase 1 (exit): el text
|
|
||||||
// marxa pels costats com el room outro. Reaprofitem computeRoomTextX
|
|
||||||
// amb phase_enter=-1 (no enter anim) i phase_exit=1.
|
|
||||||
const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING),
|
|
||||||
PHASE,
|
PHASE,
|
||||||
ELAPSED,
|
ELAPSED);
|
||||||
/*phase_enter=*/-1,
|
|
||||||
/*phase_exit=*/1,
|
|
||||||
/*dur_enter=*/0,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/false);
|
|
||||||
const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING),
|
|
||||||
PHASE,
|
|
||||||
ELAPSED,
|
|
||||||
/*phase_enter=*/-1,
|
|
||||||
/*phase_exit=*/1,
|
|
||||||
/*dur_enter=*/0,
|
|
||||||
DUR_EXIT,
|
|
||||||
/*from_right=*/true);
|
|
||||||
const int GAP = transition_config_.outro_text_line_gap_px;
|
|
||||||
drawRoomTextLine(LINE_1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE);
|
|
||||||
drawRoomTextLine(LINE_2, X_2, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE);
|
|
||||||
|
|
||||||
(void)DUR_HOLD; // Reservat per a futurs efectes de hold (pulse, etc.).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::renderGameOverFade() {
|
void GameScene::renderGameOverFade() {
|
||||||
@@ -1793,18 +1739,12 @@ void GameScene::render() {
|
|||||||
};
|
};
|
||||||
std::vector<Renderable> order;
|
std::vector<Renderable> order;
|
||||||
order.reserve(entity_manager_.entityCount() + players_.size());
|
order.reserve(entity_manager_.entityCount() + players_.size());
|
||||||
// Mergem els dos sacs (map + actors) i els jugadors en un sol
|
// Mergem entitats i jugadors en un sol vector temporal — el
|
||||||
// vector temporal — el `stable_sort` per `feet_y` posterior fa el
|
// `stable_sort` per `feet_y` posterior fa el z-order interleaving.
|
||||||
// z-order interleaving entre els tres orígens sense importar el
|
const auto ENTITIES = entity_manager_.getEntities();
|
||||||
// sac d'on venen.
|
std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) {
|
||||||
const auto MAP = entity_manager_.getMapEntities();
|
|
||||||
std::ranges::transform(MAP, std::back_inserter(order), [](Entity* e) {
|
|
||||||
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
||||||
});
|
});
|
||||||
const auto ACTORS = entity_manager_.getActors();
|
|
||||||
std::ranges::transform(ACTORS, std::back_inserter(order), [](DynamicHazard* a) -> Renderable {
|
|
||||||
return Renderable{.feet_y = a->getFeetY(), .entity = a, .player = nullptr};
|
|
||||||
});
|
|
||||||
// Tots els jugadors entren al z-sort. Els fora de joc fan no-op a
|
// Tots els jugadors entren al z-sort. Els fora de joc fan no-op a
|
||||||
// `render()`; els actius (inclosos AIRBORNE durant l'spawn aeri)
|
// `render()`; els actius (inclosos AIRBORNE durant l'spawn aeri)
|
||||||
// es pinten amb el seu feet_y normal. La sombra que afecta la
|
// es pinten amb el seu feet_y normal. La sombra que afecta la
|
||||||
|
|||||||
@@ -372,6 +372,33 @@ class GameScene : public Scene {
|
|||||||
void drawRoomTextPanel(int center_y, int panel_height) const;
|
void drawRoomTextPanel(int center_y, int panel_height) const;
|
||||||
void drawRoomTextLine(const std::string& text, int x, int y, std::uint8_t fill_color, std::uint8_t outline_color) const;
|
void drawRoomTextLine(const std::string& text, int x, int y, std::uint8_t fill_color, std::uint8_t outline_color) const;
|
||||||
|
|
||||||
|
// Especificació declarativa d'un rètol de transició de dues línies
|
||||||
|
// amb panel translúcid. ROOM_INTRO, ROOM_OUTRO, CONTINUE? i GAME OVER
|
||||||
|
// comparteixen la mateixa coreografia (panel + dues línies en
|
||||||
|
// direccions oposades) i només divergeixen en el text, els índexs
|
||||||
|
// de fase i les durades. `phase_enter = -1` desactiva l'animació
|
||||||
|
// d'entrada (cas GAME_OVER, que apareix amb el panel ja amunt).
|
||||||
|
// Una `lineN` buida indica que la línia no s'ha de pintar (cas
|
||||||
|
// CONTINUE? fora del hold, on la línia 2 mostra el countdown).
|
||||||
|
struct TextScreenSpec {
|
||||||
|
std::string line1;
|
||||||
|
std::string line2;
|
||||||
|
int phase_enter{0};
|
||||||
|
int phase_hold{0};
|
||||||
|
int phase_exit{0};
|
||||||
|
int dur_enter_ms{0};
|
||||||
|
int dur_exit_ms{0};
|
||||||
|
int line_gap_px{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Orquestrador unificat dels quatre rètols de transició. Pinta el
|
||||||
|
// panel + dues línies a partir de `spec`, deixant la decisió de
|
||||||
|
// QUÈ pintar (text, fases, durades) als callers `renderRoomText`
|
||||||
|
// i `renderGameOverText`. Sense estat propi: la fase i temps
|
||||||
|
// arriben pels paràmetres perquè la mateixa funció serveix les
|
||||||
|
// dues sub-FSMs (RoomFsm i GameOverFsm).
|
||||||
|
void renderTextScreen(const TextScreenSpec& spec, int phase, int elapsed_ms) const;
|
||||||
|
|
||||||
// Posició inicial dels jugadors derivada de `Map::door_col`. Tots
|
// Posició inicial dels jugadors derivada de `Map::door_col`. Tots
|
||||||
// els jugadors comparteixen Y (centre del corredor exterior superior);
|
// els jugadors comparteixen Y (centre del corredor exterior superior);
|
||||||
// X depèn del nombre actiu: amb un jugador queda centrat sota la
|
// X depèn del nombre actiu: amb un jugador queda centrat sota la
|
||||||
|
|||||||
@@ -40,6 +40,25 @@ namespace {
|
|||||||
// Separació extra entre camps dins d'una mateixa línia.
|
// Separació extra entre camps dins d'una mateixa línia.
|
||||||
constexpr int FIELD_GAP = 6; // px entre "Vides 3" i "Tres. 00"
|
constexpr int FIELD_GAP = 6; // px entre "Vides 3" i "Tres. 00"
|
||||||
|
|
||||||
|
// Desplaçament de l'ombra del text en píxels (sota-dreta). Petit
|
||||||
|
// perquè la font del HUD és 8 px d'alçada — qualsevol valor més
|
||||||
|
// gran tapa el següent caràcter.
|
||||||
|
constexpr int SHADOW_DX = 1;
|
||||||
|
constexpr int SHADOW_DY = 1;
|
||||||
|
|
||||||
|
// Helpers per pintar text amb ombra. Primer la sombra desplaçada,
|
||||||
|
// després el text principal al damunt — l'ordre garanteix que la
|
||||||
|
// sombra mai cobreix el text. El kerning queda al default del
|
||||||
|
// Text (1 px), igual que la resta del HUD.
|
||||||
|
void drawShadowed(Text& font, int x, int y, const std::string& s, Uint8 color, Uint8 shadow) {
|
||||||
|
font.writeColored(x + SHADOW_DX, y + SHADOW_DY, s, shadow);
|
||||||
|
font.writeColored(x, y, s, color);
|
||||||
|
}
|
||||||
|
void drawMonoShadowed(Text& font, int x, int y, const std::string& s, Uint8 color, Uint8 shadow, int cell_w) {
|
||||||
|
font.writeMonoDigits(x + SHADOW_DX, y + SHADOW_DY, s, shadow, cell_w);
|
||||||
|
font.writeMonoDigits(x, y, s, color, cell_w);
|
||||||
|
}
|
||||||
|
|
||||||
// Format zero-padded a 6 dígits per a la puntuació.
|
// Format zero-padded a 6 dígits per a la puntuació.
|
||||||
auto formatScore(int v) -> std::string {
|
auto formatScore(int v) -> std::string {
|
||||||
const int CLAMPED = std::clamp(v, 0, 999999);
|
const int CLAMPED = std::clamp(v, 0, 999999);
|
||||||
@@ -261,6 +280,16 @@ void Hud::renderBlock(BlockId id) const {
|
|||||||
void Hud::renderIdleSide(int x, int y, int w, const Block& blk, int player_index, const std::string& label) const {
|
void Hud::renderIdleSide(int x, int y, int w, const Block& blk, int player_index, const std::string& label) const {
|
||||||
const auto& p = players_[static_cast<std::size_t>(player_index)];
|
const auto& p = players_[static_cast<std::size_t>(player_index)];
|
||||||
|
|
||||||
|
// Família de color del bloc: P1 (vermell-crema-vermell-fosc) per al
|
||||||
|
// jugador 0, P2 (cian-blau-blau-marí) per al jugador 1. Mantenir el
|
||||||
|
// mateix matís a label/value/shadow dóna jerarquia visual coherent
|
||||||
|
// entre línies sense empastar (la sombra és un to fosc del mateix
|
||||||
|
// matís → efecte embossing).
|
||||||
|
const auto& ui = UiConfig::get();
|
||||||
|
const Uint8 LBL_C = (player_index == 0) ? ui.hudP1Label() : ui.hudP2Label();
|
||||||
|
const Uint8 VAL_C = (player_index == 0) ? ui.hudP1Value() : ui.hudP2Value();
|
||||||
|
const Uint8 SHD_C = (player_index == 0) ? ui.hudP1Shadow() : ui.hudP2Shadow();
|
||||||
|
|
||||||
// Slot inactiu amb prompt actiu: substituïm el render de zeros pel
|
// Slot inactiu amb prompt actiu: substituïm el render de zeros pel
|
||||||
// missatge "PULSA START / PARA JUGAR" o "ESPERA / PARA JUGAR" segons
|
// missatge "PULSA START / PARA JUGAR" o "ESPERA / PARA JUGAR" segons
|
||||||
// el mode fixat per la GameScene cada frame. El render de zeros
|
// el mode fixat per la GameScene cada frame. El render de zeros
|
||||||
@@ -275,12 +304,12 @@ void Hud::renderIdleSide(int x, int y, int w, const Block& blk, int player_index
|
|||||||
const int L2_W = font_->length(L2);
|
const int L2_W = font_->length(L2);
|
||||||
const int X1 = x + ((w - L1_W) / 2);
|
const int X1 = x + ((w - L1_W) / 2);
|
||||||
const int X2 = x + ((w - L2_W) / 2);
|
const int X2 = x + ((w - L2_W) / 2);
|
||||||
font_->writeColored(X1, y + LINE1_Y, L1, UiConfig::get().hudLabel());
|
drawShadowed(*font_, X1, y + LINE1_Y, L1, LBL_C, SHD_C);
|
||||||
font_->writeColored(X2, y + LINE2_Y, L2, UiConfig::get().hudValue());
|
drawShadowed(*font_, X2, y + LINE2_Y, L2, VAL_C, SHD_C);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Uint8 SCORE_C = (blk.flash_ms_left > 0) ? UiConfig::get().hudFlash() : UiConfig::get().hudValue();
|
const Uint8 SCORE_C = (blk.flash_ms_left > 0) ? ui.hudFlash() : VAL_C;
|
||||||
|
|
||||||
// --- Línia 1: "<label>-000000" centrada ---
|
// --- Línia 1: "<label>-000000" centrada ---
|
||||||
const std::string LBL1 = label + SEPARATOR;
|
const std::string LBL1 = label + SEPARATOR;
|
||||||
@@ -289,9 +318,9 @@ void Hud::renderIdleSide(int x, int y, int w, const Block& blk, int player_index
|
|||||||
const int SC_W = font_->lengthMonoDigits(SCORE, MONO_DIGIT_W);
|
const int SC_W = font_->lengthMonoDigits(SCORE, MONO_DIGIT_W);
|
||||||
const int LINE1_W = LBL_W + SC_W;
|
const int LINE1_W = LBL_W + SC_W;
|
||||||
int xx = x + ((w - LINE1_W) / 2);
|
int xx = x + ((w - LINE1_W) / 2);
|
||||||
font_->writeColored(xx, y + LINE1_Y, LBL1, UiConfig::get().hudLabel());
|
drawShadowed(*font_, xx, y + LINE1_Y, LBL1, LBL_C, SHD_C);
|
||||||
xx += LBL_W;
|
xx += LBL_W;
|
||||||
font_->writeMonoDigits(xx, y + LINE1_Y, SCORE, SCORE_C, MONO_DIGIT_W);
|
drawMonoShadowed(*font_, xx, y + LINE1_Y, SCORE, SCORE_C, SHD_C, MONO_DIGIT_W);
|
||||||
|
|
||||||
// --- Línia 2: "<vides>-<n> <tresors>-<n>" centrada ---
|
// --- Línia 2: "<vides>-<n> <tresors>-<n>" centrada ---
|
||||||
const std::string V_LBL = Locale::get().text("hud.lives") + SEPARATOR;
|
const std::string V_LBL = Locale::get().text("hud.lives") + SEPARATOR;
|
||||||
@@ -304,16 +333,21 @@ void Hud::renderIdleSide(int x, int y, int w, const Block& blk, int player_index
|
|||||||
const int T_VW = font_->lengthMonoDigits(T_VAL, MONO_DIGIT_W);
|
const int T_VW = font_->lengthMonoDigits(T_VAL, MONO_DIGIT_W);
|
||||||
const int LINE2_W = V_LW + V_VW + FIELD_GAP + T_LW + T_VW;
|
const int LINE2_W = V_LW + V_VW + FIELD_GAP + T_LW + T_VW;
|
||||||
xx = x + ((w - LINE2_W) / 2);
|
xx = x + ((w - LINE2_W) / 2);
|
||||||
font_->writeColored(xx, y + LINE2_Y, V_LBL, UiConfig::get().hudLabel());
|
drawShadowed(*font_, xx, y + LINE2_Y, V_LBL, LBL_C, SHD_C);
|
||||||
xx += V_LW;
|
xx += V_LW;
|
||||||
font_->writeMonoDigits(xx, y + LINE2_Y, V_VAL, UiConfig::get().hudValue(), MONO_DIGIT_W);
|
drawMonoShadowed(*font_, xx, y + LINE2_Y, V_VAL, VAL_C, SHD_C, MONO_DIGIT_W);
|
||||||
xx += V_VW + FIELD_GAP;
|
xx += V_VW + FIELD_GAP;
|
||||||
font_->writeColored(xx, y + LINE2_Y, T_LBL, UiConfig::get().hudLabel());
|
drawShadowed(*font_, xx, y + LINE2_Y, T_LBL, LBL_C, SHD_C);
|
||||||
xx += T_LW;
|
xx += T_LW;
|
||||||
font_->writeMonoDigits(xx, y + LINE2_Y, T_VAL, UiConfig::get().hudValue(), MONO_DIGIT_W);
|
drawMonoShadowed(*font_, xx, y + LINE2_Y, T_VAL, VAL_C, SHD_C, MONO_DIGIT_W);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Hud::renderIdleCenter(int x, int y, int w) const {
|
void Hud::renderIdleCenter(int x, int y, int w) const {
|
||||||
|
const auto& ui = UiConfig::get();
|
||||||
|
const Uint8 LBL_C = ui.hudCenterLabel();
|
||||||
|
const Uint8 VAL_C = ui.hudCenterValue();
|
||||||
|
const Uint8 SHD_C = ui.hudCenterShadow();
|
||||||
|
|
||||||
// --- Línia 1: "<piramide>-<n>" centrada ---
|
// --- Línia 1: "<piramide>-<n>" centrada ---
|
||||||
const std::string PIR_LBL = Locale::get().text("hud.pyramid") + SEPARATOR;
|
const std::string PIR_LBL = Locale::get().text("hud.pyramid") + SEPARATOR;
|
||||||
const std::string PIR_VAL = std::to_string(pyramid_);
|
const std::string PIR_VAL = std::to_string(pyramid_);
|
||||||
@@ -321,9 +355,9 @@ void Hud::renderIdleCenter(int x, int y, int w) const {
|
|||||||
const int PV_W = font_->lengthMonoDigits(PIR_VAL, MONO_DIGIT_W);
|
const int PV_W = font_->lengthMonoDigits(PIR_VAL, MONO_DIGIT_W);
|
||||||
const int LINE1_W = PL_W + PV_W;
|
const int LINE1_W = PL_W + PV_W;
|
||||||
int xx = x + ((w - LINE1_W) / 2);
|
int xx = x + ((w - LINE1_W) / 2);
|
||||||
font_->writeColored(xx, y + LINE1_Y, PIR_LBL, UiConfig::get().hudLabel());
|
drawShadowed(*font_, xx, y + LINE1_Y, PIR_LBL, LBL_C, SHD_C);
|
||||||
xx += PL_W;
|
xx += PL_W;
|
||||||
font_->writeMonoDigits(xx, y + LINE1_Y, PIR_VAL, UiConfig::get().hudValue(), MONO_DIGIT_W);
|
drawMonoShadowed(*font_, xx, y + LINE1_Y, PIR_VAL, VAL_C, SHD_C, MONO_DIGIT_W);
|
||||||
|
|
||||||
// --- Línia 2: "<sala>-<r>" centrada ---
|
// --- Línia 2: "<sala>-<r>" centrada ---
|
||||||
const std::string SALA_LBL = Locale::get().text("hud.room") + SEPARATOR;
|
const std::string SALA_LBL = Locale::get().text("hud.room") + SEPARATOR;
|
||||||
@@ -332,7 +366,7 @@ void Hud::renderIdleCenter(int x, int y, int w) const {
|
|||||||
const int SV_W = font_->lengthMonoDigits(SALA_VAL, MONO_DIGIT_W);
|
const int SV_W = font_->lengthMonoDigits(SALA_VAL, MONO_DIGIT_W);
|
||||||
const int LINE2_W = SL_W + SV_W;
|
const int LINE2_W = SL_W + SV_W;
|
||||||
xx = x + ((w - LINE2_W) / 2);
|
xx = x + ((w - LINE2_W) / 2);
|
||||||
font_->writeColored(xx, y + LINE2_Y, SALA_LBL, UiConfig::get().hudLabel());
|
drawShadowed(*font_, xx, y + LINE2_Y, SALA_LBL, LBL_C, SHD_C);
|
||||||
xx += SL_W;
|
xx += SL_W;
|
||||||
font_->writeMonoDigits(xx, y + LINE2_Y, SALA_VAL, UiConfig::get().hudValue(), MONO_DIGIT_W);
|
drawMonoShadowed(*font_, xx, y + LINE2_Y, SALA_VAL, VAL_C, SHD_C, MONO_DIGIT_W);
|
||||||
}
|
}
|
||||||
@@ -217,20 +217,28 @@ namespace Defaults {
|
|||||||
constexpr Uint8 MENU_DISABLED = 16; // Label en gris quan no aplica.
|
constexpr Uint8 MENU_DISABLED = 16; // Label en gris quan no aplica.
|
||||||
|
|
||||||
// Índexs de paleta per al marcador de joc (HUD de la GameScene).
|
// Índexs de paleta per al marcador de joc (HUD de la GameScene).
|
||||||
// Arranca reciclant els colors del menú per coherència visual;
|
// Tres "famílies" de color — un parell (label, value) + sombra per
|
||||||
// separant els noms a constants pròpies, tocar-los a posteriori
|
// cada bloc — donen jerarquia visual: P1 càlid (vermell+crema),
|
||||||
// no afecta el menú.
|
// CENTER ambiental (ambre+or), P2 fred (cian+blau pàl·lid). La
|
||||||
constexpr Uint8 HUD_BG = 0; // Fons de cada bloc.
|
// sombra és un to més fosc del mateix matís per donar volum.
|
||||||
constexpr Uint8 HUD_LABEL = 58; // Etiquetes ("P1", "V", "T").
|
constexpr Uint8 HUD_BG = 0; // Fons de cada bloc.
|
||||||
constexpr Uint8 HUD_VALUE = 187; // Valors (score, vides, tresors).
|
constexpr Uint8 HUD_P1_LABEL = 7; // P1 label: vermell pur (#ff0000).
|
||||||
constexpr Uint8 HUD_FLASH = 237; // Color temporal del valor en flash.
|
constexpr Uint8 HUD_P1_VALUE = 183; // P1 value: crema-préssec (#ffdaaa).
|
||||||
|
constexpr Uint8 HUD_P1_SHADOW = 2; // P1 ombra: vermell fosc (#480000).
|
||||||
|
constexpr Uint8 HUD_CENTER_LABEL = 29; // CENTER label: ambre cremat (#b66d00).
|
||||||
|
constexpr Uint8 HUD_CENTER_VALUE = 55; // CENTER value: or brillant (#ffda00).
|
||||||
|
constexpr Uint8 HUD_CENTER_SHADOW = 10; // CENTER ombra: marró cremat (#482400).
|
||||||
|
constexpr Uint8 HUD_P2_LABEL = 248; // P2 label: cian pur (#00ffff).
|
||||||
|
constexpr Uint8 HUD_P2_VALUE = 253; // P2 value: blau pàl·lid (#b6ffff).
|
||||||
|
constexpr Uint8 HUD_P2_SHADOW = 64; // P2 ombra: blau marí (#000055).
|
||||||
|
constexpr Uint8 HUD_FLASH = 63; // Flash (groc neó, comú a totes les famílies).
|
||||||
|
|
||||||
// Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO).
|
// Rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO).
|
||||||
// Independent dels colors de notificació perquè el rètol de
|
// Comparteix la família CENTER del HUD: or sobre marró cremat,
|
||||||
// fase és un canal visual diferent. `ROOM_TEXT_OUTLINE` és el
|
// per coherència visual ("Piràmide N / Sala N" és sempre
|
||||||
// contorn que abans estava hardcodejat al `renderRoomText`.
|
// daurat arreu del joc).
|
||||||
constexpr Uint8 ROOM_TEXT = 232;
|
constexpr Uint8 ROOM_TEXT = 55; // Or brillant (= HUD_CENTER_VALUE).
|
||||||
constexpr Uint8 ROOM_TEXT_OUTLINE = 255;
|
constexpr Uint8 ROOM_TEXT_OUTLINE = 10; // Marró cremat (= HUD_CENTER_SHADOW).
|
||||||
|
|
||||||
// Timings d'animació del HUD. L'entrada són els tres blocs lliscant
|
// Timings d'animació del HUD. L'entrada són els tres blocs lliscant
|
||||||
// a la seua posició; flash i shake acompanyen un `player_hit`.
|
// a la seua posició; flash i shake acompanyen un `player_hit`.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace Texts {
|
namespace Texts {
|
||||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte ARCADE — JailDesigner";
|
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte ARCADE — JailDesigner";
|
||||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||||
constexpr const char* VERSION = "0.41.0"; // Single source of truth — Makefile el grep
|
constexpr const char* VERSION = "0.43.0"; // Single source of truth — Makefile el grep
|
||||||
} // namespace Texts
|
} // namespace Texts
|
||||||
|
|
||||||
namespace GameCanvas {
|
namespace GameCanvas {
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ auto Audio::get() -> Audio* {
|
|||||||
void Audio::playSound(const std::string& /*name*/, Audio::Group /*group*/) {
|
void Audio::playSound(const std::string& /*name*/, Audio::Group /*group*/) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Audio::playSound(const std::string& /*name*/, Audio::Group /*group*/, float /*speed*/) {
|
||||||
|
}
|
||||||
|
|
||||||
namespace Resource {
|
namespace Resource {
|
||||||
|
|
||||||
auto Cache::get() -> Cache& {
|
auto Cache::get() -> Cache& {
|
||||||
|
|||||||
@@ -69,13 +69,14 @@ namespace {
|
|||||||
|
|
||||||
#define REQUIRE(cond) check((cond), #cond, __FILE__, __LINE__)
|
#define REQUIRE(cond) check((cond), #cond, __FILE__, __LINE__)
|
||||||
|
|
||||||
// Treu la mòmia de l'estat SPAWN inicial (Mummy::Mummy ara hi entra).
|
// Treu la mòmia de l'estat SPAWN inicial (Mummy::Mummy ara hi entra
|
||||||
// Equival al que fa GameScene al final del text-exit de ROOM_INTRO: arma
|
// amb l'animació `raise` activa). Tickem amb dt gran fins sortir de
|
||||||
// el comptador i consumeix la durada amb un update. Tests que operin
|
// SPAWNING — mateix path que `Mummy::update` executa al final de
|
||||||
// directament sobre HURT/ROAMING l'han de cridar abans.
|
// l'animació en partida real. El for limita el bucle defensivament.
|
||||||
void exitMummySpawn(Mummy& mummy) {
|
void exitMummySpawn(Mummy& mummy) {
|
||||||
mummy.triggerSpawnExit();
|
for (int i = 0; i < 32 && mummy.isSpawning(); ++i) {
|
||||||
mummy.update(mummy.getSpawnDurationS() + 0.001F);
|
mummy.update(0.5F);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void testFactory() {
|
void testFactory() {
|
||||||
@@ -165,9 +166,11 @@ namespace {
|
|||||||
REQUIRE(sarc->getVisitedCount() == 8);
|
REQUIRE(sarc->getVisitedCount() == 8);
|
||||||
REQUIRE(sarc->getState() == Sarcophagus::State::OPENING);
|
REQUIRE(sarc->getState() == Sarcophagus::State::OPENING);
|
||||||
|
|
||||||
// Un update amb l'animació `opening` d'1 frame loop=false completa
|
// Tickem prou per consumir l'animació `opening` (4 frames @ 6fps =
|
||||||
// la transició a OPEN.
|
// ~0.67s). Quan completa, la transició OPENING → OPEN es dispara.
|
||||||
sarc->update(0.0F);
|
for (int i = 0; i < 16 && sarc->getState() == Sarcophagus::State::OPENING; ++i) {
|
||||||
|
sarc->update(0.1F);
|
||||||
|
}
|
||||||
REQUIRE(sarc->getState() == Sarcophagus::State::OPEN);
|
REQUIRE(sarc->getState() == Sarcophagus::State::OPEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,12 +210,14 @@ namespace {
|
|||||||
forceOpen(s1);
|
forceOpen(s1);
|
||||||
forceOpen(s2);
|
forceOpen(s2);
|
||||||
|
|
||||||
// Un primer tick: cada sarc transiciona OPENING → OPEN i publica;
|
// Tickem prou per consumir l'animació `opening` dels sarcs (4
|
||||||
// l'handler del manager compta i, en arribar al total, crida
|
// frames @ 6fps) i la de la porta. Cada sarc transiciona
|
||||||
// door->open() — que en aquest mateix tick (la iteració sobre
|
// OPENING → OPEN i publica; l'handler del manager compta i, en
|
||||||
// entities continua) farà transitar la porta OPENING → OPEN perquè
|
// arribar al total, crida `door->open()`, que també necessita
|
||||||
// l'animació és d'1 frame loop=false.
|
// tickar fins acabar la seua animació d'obertura.
|
||||||
manager.update(0.0F);
|
for (int i = 0; i < 20 && door_ptr->getState() != Door::State::OPEN; ++i) {
|
||||||
|
manager.update(0.1F);
|
||||||
|
}
|
||||||
REQUIRE(manager.openedSarcophagusCount() == 2);
|
REQUIRE(manager.openedSarcophagusCount() == 2);
|
||||||
REQUIRE(door_ptr->getState() == Door::State::OPEN);
|
REQUIRE(door_ptr->getState() == Door::State::OPEN);
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,15 @@ namespace {
|
|||||||
|
|
||||||
#define REQUIRE(cond) check((cond), #cond, __FILE__, __LINE__)
|
#define REQUIRE(cond) check((cond), #cond, __FILE__, __LINE__)
|
||||||
|
|
||||||
// Treu la mòmia de l'estat SPAWN inicial (Mummy::Mummy ara hi entra). Els
|
// Treu la mòmia de l'estat SPAWN inicial (Mummy::Mummy ara hi entra
|
||||||
// tests del whip necessiten que la mòmia estigui ACTIVE per reaccionar
|
// amb l'animació `raise` activa). Els tests del whip necessiten que la
|
||||||
// al fueteig — sense això, `enterHurt` torna early per `isSpawning()`.
|
// mòmia estigui ACTIVE per reaccionar al fueteig — sense això,
|
||||||
|
// `enterHurt` torna early per `isSpawning()`. Tickem amb dt gran fins
|
||||||
|
// sortir de SPAWNING (mateix path real, només accelerat).
|
||||||
void exitMummySpawn(Mummy& mummy) {
|
void exitMummySpawn(Mummy& mummy) {
|
||||||
mummy.triggerSpawnExit();
|
for (int i = 0; i < 32 && mummy.isSpawning(); ++i) {
|
||||||
mummy.update(mummy.getSpawnDurationS() + 0.001F);
|
mummy.update(0.5F);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caja de jugador "estàndard" AEE: 16×32 amb cantonada (x, y).
|
// Caja de jugador "estàndard" AEE: 16×32 amb cantonada (x, y).
|
||||||
|
|||||||