merge: neteja tidy AEE (404→0)

This commit is contained in:
2026-05-16 16:16:36 +02:00
92 changed files with 3316 additions and 3561 deletions
+31 -31
View File
@@ -40,15 +40,15 @@ Audio::Audio() { initSDLAudio(); }
// Destructor
Audio::~Audio() {
JA_Quit();
Ja::quit();
}
// Método principal
void Audio::update() {
JA_Update();
Ja::update();
// Sincronizar estado: detectar cuando la música se para (ej. fade-out completado)
if (instance && instance->music_.state == MusicState::PLAYING && JA_GetMusicState() != JA_MUSIC_PLAYING) {
if (instance && instance->music_.state == MusicState::PLAYING && Ja::getMusicState() != Ja::MusicState::PLAYING) {
instance->music_.state = MusicState::STOPPED;
}
}
@@ -72,12 +72,12 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
}
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
JA_CrossfadeMusic(resource, crossfade_ms, loop);
Ja::crossfadeMusic(resource, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
JA_StopMusic();
Ja::stopMusic();
}
JA_PlayMusic(resource, loop);
Ja::playMusic(resource, loop);
}
music_.name = name;
@@ -86,18 +86,18 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
}
// Reproduce la música por puntero (con crossfade opcional)
void Audio::playMusic(JA_Music_t* music, const int loop, const int crossfade_ms) {
void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) {
if (!music_enabled_ || music == nullptr) {
return;
}
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
JA_CrossfadeMusic(music, crossfade_ms, loop);
Ja::crossfadeMusic(music, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
JA_StopMusic();
Ja::stopMusic();
}
JA_PlayMusic(music, loop);
Ja::playMusic(music, loop);
}
music_.name.clear(); // nom desconegut quan es passa per punter
@@ -108,7 +108,7 @@ void Audio::playMusic(JA_Music_t* music, const int loop, const int crossfade_ms)
// Pausa la música
void Audio::pauseMusic() {
if (music_enabled_ && music_.state == MusicState::PLAYING) {
JA_PauseMusic();
Ja::pauseMusic();
music_.state = MusicState::PAUSED;
}
}
@@ -116,7 +116,7 @@ void Audio::pauseMusic() {
// Continua la música pausada
void Audio::resumeMusic() {
if (music_enabled_ && music_.state == MusicState::PAUSED) {
JA_ResumeMusic();
Ja::resumeMusic();
music_.state = MusicState::PLAYING;
}
}
@@ -124,7 +124,7 @@ void Audio::resumeMusic() {
// Detiene la música
void Audio::stopMusic() {
if (music_enabled_) {
JA_StopMusic();
Ja::stopMusic();
music_.state = MusicState::STOPPED;
}
}
@@ -132,42 +132,42 @@ void Audio::stopMusic() {
// Reproduce un sonido por nombre
void Audio::playSound(const std::string& name, Group group) const {
if (sound_enabled_) {
JA_PlaySound(AudioResource::getSound(name), 0, static_cast<int>(group));
Ja::playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
}
}
// Reproduce un sonido por puntero directo
void Audio::playSound(JA_Sound_t* sound, Group group) const {
void Audio::playSound(Ja::Sound* sound, Group group) const {
if (sound_enabled_ && sound != nullptr) {
JA_PlaySound(sound, 0, static_cast<int>(group));
Ja::playSound(sound, 0, static_cast<int>(group));
}
}
// Detiene todos los sonidos
void Audio::stopAllSounds() const {
if (sound_enabled_) {
JA_StopChannel(-1);
Ja::stopChannel(-1);
}
}
// Realiza un fundido de salida de la música
void Audio::fadeOutMusic(int milliseconds) const {
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
JA_FadeOutMusic(milliseconds);
Ja::fadeOutMusic(milliseconds);
}
}
// Consulta directamente el estado real de la música en jailaudio
auto Audio::getRealMusicState() -> MusicState {
JA_Music_state ja_state = JA_GetMusicState();
Ja::MusicState ja_state = Ja::getMusicState();
switch (ja_state) {
case JA_MUSIC_PLAYING:
case Ja::MusicState::PLAYING:
return MusicState::PLAYING;
case JA_MUSIC_PAUSED:
case Ja::MusicState::PAUSED:
return MusicState::PAUSED;
case JA_MUSIC_STOPPED:
case JA_MUSIC_INVALID:
case JA_MUSIC_DISABLED:
case Ja::MusicState::STOPPED:
case Ja::MusicState::INVALID:
case Ja::MusicState::DISABLED:
default:
return MusicState::STOPPED;
}
@@ -176,17 +176,17 @@ auto Audio::getRealMusicState() -> MusicState {
// Establece el volumen de los sonidos (float 0.0..1.0)
void Audio::setSoundVolume(float sound_volume, Group group) const {
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
const bool active = enabled_ && sound_enabled_;
const float CONVERTED_VOLUME = active ? sound_volume * Options::audio.volume : 0.0F;
JA_SetSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
const bool ACTIVE = enabled_ && sound_enabled_;
const float CONVERTED_VOLUME = ACTIVE ? sound_volume * Options::audio.volume : 0.0F;
Ja::setSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
}
// Establece el volumen de la música (float 0.0..1.0)
void Audio::setMusicVolume(float music_volume) const {
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
const bool active = enabled_ && music_enabled_;
const float CONVERTED_VOLUME = active ? music_volume * Options::audio.volume : 0.0F;
JA_SetMusicVolume(CONVERTED_VOLUME);
const bool ACTIVE = enabled_ && music_enabled_;
const float CONVERTED_VOLUME = ACTIVE ? music_volume * Options::audio.volume : 0.0F;
Ja::setMusicVolume(CONVERTED_VOLUME);
}
// Aplica la configuración
@@ -207,7 +207,7 @@ void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) {
std::cout << "SDL_AUDIO could not initialize! SDL Error: " << SDL_GetError() << '\n';
} else {
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
Ja::init(FREQUENCY, SDL_AUDIO_S16LE, 2);
enable(Options::audio.enabled);
}
}
+7 -2
View File
@@ -6,6 +6,11 @@
#include <string> // Para string
#include <utility> // Para move
namespace Ja {
struct Music;
struct Sound;
} // namespace Ja
// --- Clase Audio: gestor de audio (singleton) ---
// Implementació canònica, byte-idèntica entre projectes.
// Els volums es manegen internament com a float 0.01.0; la capa de
@@ -45,7 +50,7 @@ class Audio {
// --- Control de música ---
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
void playMusic(struct JA_Music_t* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional)
void playMusic(Ja::Music* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional)
void pauseMusic(); // Pausar reproducción de música
void resumeMusic(); // Continua la música pausada
void stopMusic(); // Detener completamente la música
@@ -53,7 +58,7 @@ class Audio {
// --- Control de sonidos ---
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
void playSound(Ja::Sound* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
void stopAllSounds() const; // Detener todos los sonidos
// --- Control de volumen (API interna: float 0.0..1.0) ---
+2 -2
View File
@@ -4,11 +4,11 @@
namespace AudioResource {
auto getMusic(const std::string& name) -> JA_Music_t* {
auto getMusic(const std::string& name) -> Ja::Music* {
return Resource::Cache::get()->getMusic(name);
}
auto getSound(const std::string& name) -> JA_Sound_t* {
auto getSound(const std::string& name) -> Ja::Sound* {
return Resource::Cache::get()->getSound(name);
}
+8 -6
View File
@@ -1,17 +1,19 @@
#pragma once
// --- Audio Resource Adapter ---
// Aquest fitxer exposa una interfície comuna a Audio per obtenir JA_Music_t* /
// JA_Sound_t* per nom. Cada projecte la implementa en audio_adapter.cpp
// Aquest fitxer exposa una interfície comuna a Audio per obtenir Ja::Music* /
// Ja::Sound* per nom. Cada projecte la implementa en audio_adapter.cpp
// delegant al seu singleton de recursos (Resource::get(), Resource::Cache::get(),
// etc.). Això permet que audio.hpp/audio.cpp siguin idèntics entre projectes.
#include <string> // Para string
struct JA_Music_t;
struct JA_Sound_t;
namespace Ja {
struct Music;
struct Sound;
} // namespace Ja
namespace AudioResource {
auto getMusic(const std::string& name) -> JA_Music_t*;
auto getSound(const std::string& name) -> JA_Sound_t*;
auto getMusic(const std::string& name) -> Ja::Music*;
auto getSound(const std::string& name) -> Ja::Sound*;
} // namespace AudioResource
File diff suppressed because it is too large Load Diff
+112 -123
View File
@@ -11,8 +11,8 @@
namespace Gamepad {
static SDL_Gamepad* pad_ = nullptr;
static SDL_JoystickID pad_id_ = 0;
static SDL_Gamepad* pad = nullptr;
static SDL_JoystickID pad_id = 0;
// Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos web (el gamepad.id
// de Chrome/Android no porta Vendor/Product, el parser extreu valors
@@ -54,9 +54,9 @@ namespace Gamepad {
// elimina espais finals i talla a 25 caràcters.
static auto prettyName(const char* raw) -> std::string {
std::string name = ((raw != nullptr) && (*raw != 0)) ? raw : "Gamepad";
const auto pos = name.find_first_of("([");
if (pos != std::string::npos) {
name.erase(pos);
const auto POS = name.find_first_of("([");
if (POS != std::string::npos) {
name.erase(POS);
}
while (!name.empty() && name.back() == ' ') {
name.pop_back();
@@ -74,16 +74,16 @@ namespace Gamepad {
static constexpr Sint16 STICK_DEADZONE = 12000;
// Estat previ per a detecció de flanc (edge-triggered)
static bool prev_up_ = false;
static bool prev_down_ = false;
static bool prev_left_ = false;
static bool prev_right_ = false;
static bool prev_south_ = false;
static bool prev_east_ = false;
static bool prev_west_ = false;
static bool prev_north_ = false;
static bool prev_start_ = false;
static bool prev_back_ = false;
static bool prev_up = false;
static bool prev_down = false;
static bool prev_left = false;
static bool prev_right = false;
static bool prev_south = false;
static bool prev_east = false;
static bool prev_west = false;
static bool prev_north = false;
static bool prev_start = false;
static bool prev_back = false;
static void notify(const std::string& name, const char* status_key) {
std::string msg = name.empty() ? "Gamepad" : name;
@@ -106,10 +106,10 @@ namespace Gamepad {
if (!SDL_IsGamepad(ids[i])) {
continue;
}
pad_ = SDL_OpenGamepad(ids[i]);
if (pad_ != nullptr) {
pad_id_ = ids[i];
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_));
pad = SDL_OpenGamepad(ids[i]);
if (pad != nullptr) {
pad_id = ids[i];
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad));
break;
}
}
@@ -132,16 +132,16 @@ namespace Gamepad {
}
void destroy() {
if (pad_ != nullptr) {
SDL_CloseGamepad(pad_);
pad_ = nullptr;
pad_id_ = 0;
if (pad != nullptr) {
SDL_CloseGamepad(pad);
pad = nullptr;
pad_id = 0;
}
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
}
auto isConnected() -> bool {
return pad_ != nullptr;
return pad != nullptr;
}
void handleEvent(const SDL_Event& event) {
@@ -149,32 +149,32 @@ namespace Gamepad {
// GAMEPAD_ADDED) perquè SDL no reconeix el GUID. Escoltem els dos i
// injectem el mapping estàndard abans d'obrir el mando.
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_JOYSTICK_ADDED) {
if (pad_ == nullptr) {
if (pad == nullptr) {
SDL_JoystickID jid = event.jdevice.which;
installWebStandardMapping(jid);
if (!SDL_IsGamepad(jid)) {
return;
}
pad_ = SDL_OpenGamepad(jid);
if (pad_ != nullptr) {
pad_id_ = jid;
std::string name = prettyName(SDL_GetGamepadName(pad_));
pad = SDL_OpenGamepad(jid);
if (pad != nullptr) {
pad_id = jid;
std::string name = prettyName(SDL_GetGamepadName(pad));
SDL_Log("Gamepad connectat: %s", name.c_str());
notifyConnected(name);
}
}
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
if ((pad_ != nullptr) && event.jdevice.which == pad_id_) {
std::string saved_name = prettyName(SDL_GetGamepadName(pad_));
if ((pad != nullptr) && event.jdevice.which == pad_id) {
std::string saved_name = prettyName(SDL_GetGamepadName(pad));
SDL_Log("Gamepad desconnectat: %s", saved_name.c_str());
SDL_CloseGamepad(pad_);
pad_ = nullptr;
pad_id_ = 0;
SDL_CloseGamepad(pad);
pad = nullptr;
pad_id = 0;
// Neteja qualsevol tecla virtual que poguera estar premuda
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false);
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false);
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false);
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, false);
Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, false);
Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, false);
Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, false);
Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, false);
notifyDisconnected(saved_name);
}
}
@@ -195,97 +195,86 @@ namespace Gamepad {
SDL_PushEvent(&e);
}
// Estat agregat d'un frame: D-pad i stick combinats, més botons frontals.
struct PadState {
bool up;
bool down;
bool left;
bool right;
bool south;
bool east;
bool west;
bool north;
bool start;
bool back;
};
static auto readPadState() -> PadState {
const Sint16 LX = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX);
const Sint16 LY = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY);
return PadState{
.up = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_UP) || LY < -STICK_DEADZONE,
.down = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_DOWN) || LY > STICK_DEADZONE,
.left = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT) || LX < -STICK_DEADZONE,
.right = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT) || LX > STICK_DEADZONE,
.south = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_SOUTH),
.east = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_EAST),
.west = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_WEST),
.north = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_NORTH),
.start = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_START),
.back = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_BACK),
};
}
static void handleMenuNavigation(const PadState& s) {
if (s.up && !prev_up) { pushKey(SDL_SCANCODE_UP); }
if (s.down && !prev_down) { pushKey(SDL_SCANCODE_DOWN); }
if (s.left && !prev_left) { pushKey(SDL_SCANCODE_LEFT); }
if (s.right && !prev_right) { pushKey(SDL_SCANCODE_RIGHT); }
if (s.east && !prev_east) { pushKey(SDL_SCANCODE_RETURN); }
if (s.south && !prev_south) { pushKey(SDL_SCANCODE_BACKSPACE); }
// Mentre el menú està obert, el joc no ha de rebre moviment.
Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, false);
Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, false);
Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, false);
Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, false);
}
static void handleGameInput(const PadState& s) {
Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, s.up);
Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, s.down);
Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, s.left);
Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, s.right);
const bool ANY_FRONT_EDGE = (s.south && !prev_south) || (s.east && !prev_east) ||
(s.west && !prev_west) || (s.north && !prev_north);
if (ANY_FRONT_EDGE) {
pushKey(SDL_SCANCODE_RETURN);
}
}
void update() {
if (pad_ == nullptr) {
if (pad == nullptr) {
return;
}
// D-pad
bool dup = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_UP);
bool ddn = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
bool dlt = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
bool drt = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
// Stick esquerre amb dead-zone
Sint16 lx = SDL_GetGamepadAxis(pad_, SDL_GAMEPAD_AXIS_LEFTX);
Sint16 ly = SDL_GetGamepadAxis(pad_, SDL_GAMEPAD_AXIS_LEFTY);
bool sup = ly < -STICK_DEADZONE;
bool sdn = ly > STICK_DEADZONE;
bool slt = lx < -STICK_DEADZONE;
bool srt = lx > STICK_DEADZONE;
bool up = dup || sup;
bool dn = ddn || sdn;
bool lt = dlt || slt;
bool rt = drt || srt;
// Botons frontals (layout SDL: SOUTH=A/Cross, EAST=B/Circle, WEST=X/Square, NORTH=Y/Triangle)
bool south = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_SOUTH);
bool east = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_EAST);
bool west = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_WEST);
bool north = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_NORTH);
bool start = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_START);
bool back = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_BACK);
// Select (Back) → obre/tanca menú de servei (flanc)
if (back && !prev_back_) {
pushKey(KeyConfig::scancode("menu_toggle"));
}
// Start → pausa (flanc)
if (start && !prev_start_) {
pushKey(KeyConfig::scancode("pause_toggle"));
}
const PadState S = readPadState();
// Flancs globals: Select i Start sempre operen.
if (S.back && !prev_back) { pushKey(KeyConfig::scancode("menu_toggle")); }
if (S.start && !prev_start) { pushKey(KeyConfig::scancode("pause_toggle")); }
if (Menu::isOpen()) {
// Navegació del menú per flanc
if (up && !prev_up_) {
pushKey(SDL_SCANCODE_UP);
}
if (dn && !prev_down_) {
pushKey(SDL_SCANCODE_DOWN);
}
if (lt && !prev_left_) {
pushKey(SDL_SCANCODE_LEFT);
}
if (rt && !prev_right_) {
pushKey(SDL_SCANCODE_RIGHT);
}
// EAST accepta, SOUTH cancela / endarrere
if (east && !prev_east_) {
pushKey(SDL_SCANCODE_RETURN);
}
if (south && !prev_south_) {
pushKey(SDL_SCANCODE_BACKSPACE);
}
// Assegura que el joc no rep tecles de moviment mentre el menú està obert
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false);
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false);
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false);
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, false);
handleMenuNavigation(S);
} else {
// Moviment al joc — level-triggered (polling)
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, up);
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, dn);
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, lt);
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, rt);
// Qualsevol dels 4 botons frontals avança escenes (JI_AnyKey via Enter sintètic)
if ((south && !prev_south_) || (east && !prev_east_) ||
(west && !prev_west_) || (north && !prev_north_)) {
pushKey(SDL_SCANCODE_RETURN);
}
handleGameInput(S);
}
prev_up_ = up;
prev_down_ = dn;
prev_left_ = lt;
prev_right_ = rt;
prev_south_ = south;
prev_east_ = east;
prev_west_ = west;
prev_north_ = north;
prev_start_ = start;
prev_back_ = back;
prev_up = S.up;
prev_down = S.down;
prev_left = S.left;
prev_right = S.right;
prev_south = S.south;
prev_east = S.east;
prev_west = S.west;
prev_north = S.north;
prev_start = S.start;
prev_back = S.back;
}
} // namespace Gamepad
+32 -91
View File
@@ -1,6 +1,7 @@
#include "core/input/global_inputs.hpp"
#include <cstdio>
#include <functional>
#include <string>
#include "core/input/key_config.hpp"
@@ -23,131 +24,71 @@ namespace GlobalInputs {
static bool texture_filter_prev = false;
static bool render_info_prev = false;
// Patró comú: lectura amb detecció de flanc + acumulació al flag "consumed".
// `on_press` només s'executa al flanc puja; `prev` es manté actualitzat.
static auto edgeTrigger(const char* key_id, bool& prev, const std::function<void()>& on_press) -> bool {
const bool PRESSED = Ji::keyPressed(KeyConfig::scancode(key_id));
if (PRESSED && !prev) {
on_press();
}
prev = PRESSED;
return PRESSED;
}
auto handle() -> bool {
bool consumed = false;
// F1 — Reduir zoom
bool dec_zoom = JI_KeyPressed(KeyConfig::scancode("dec_zoom"));
if (dec_zoom && !dec_zoom_prev) {
consumed |= edgeTrigger("dec_zoom", dec_zoom_prev, [] {
Screen::get()->decZoom();
char msg[32];
snprintf(msg, sizeof(msg), Locale::get("notifications.zoom_fmt"), Screen::get()->getZoom());
Overlay::showNotification(msg);
}
if (dec_zoom) {
consumed = true;
}
dec_zoom_prev = dec_zoom;
// F2 — Augmentar zoom
bool inc_zoom = JI_KeyPressed(KeyConfig::scancode("inc_zoom"));
if (inc_zoom && !inc_zoom_prev) {
});
consumed |= edgeTrigger("inc_zoom", inc_zoom_prev, [] {
Screen::get()->incZoom();
char msg[32];
snprintf(msg, sizeof(msg), Locale::get("notifications.zoom_fmt"), Screen::get()->getZoom());
Overlay::showNotification(msg);
}
if (inc_zoom) {
consumed = true;
}
inc_zoom_prev = inc_zoom;
// F3 — Toggle pantalla completa
bool fullscreen = JI_KeyPressed(KeyConfig::scancode("fullscreen"));
if (fullscreen && !fullscreen_prev) {
});
consumed |= edgeTrigger("fullscreen", fullscreen_prev, [] {
Screen::get()->toggleFullscreen();
Overlay::showNotification(Screen::get()->isFullscreen() ? Locale::get("notifications.fullscreen") : Locale::get("notifications.windowed"));
}
if (fullscreen) {
consumed = true;
}
fullscreen_prev = fullscreen;
// F4 — Toggle shaders
bool shader = JI_KeyPressed(KeyConfig::scancode("toggle_shader"));
if (shader && !shader_prev) {
});
consumed |= edgeTrigger("toggle_shader", shader_prev, [] {
Screen::get()->toggleShaders();
Overlay::showNotification(Options::video.shader_enabled ? Locale::get("notifications.shader_on") : Locale::get("notifications.shader_off"));
}
if (shader) {
consumed = true;
}
shader_prev = shader;
// F5 — Toggle aspect ratio 4:3
bool aspect = JI_KeyPressed(KeyConfig::scancode("toggle_aspect_ratio"));
if (aspect && !aspect_prev) {
});
consumed |= edgeTrigger("toggle_aspect_ratio", aspect_prev, [] {
Screen::get()->toggleAspectRatio();
Overlay::showNotification(Options::video.aspect_ratio_4_3 ? Locale::get("notifications.aspect_43") : Locale::get("notifications.aspect_square"));
}
if (aspect) {
consumed = true;
}
aspect_prev = aspect;
// F6 — Toggle supersampling
bool ss = JI_KeyPressed(KeyConfig::scancode("toggle_supersampling"));
if (ss && !ss_prev) {
});
consumed |= edgeTrigger("toggle_supersampling", ss_prev, [] {
if (Screen::get()->toggleSupersampling()) {
Overlay::showNotification(Options::video.supersampling ? Locale::get("notifications.ss_on") : Locale::get("notifications.ss_off"));
}
}
if (ss) {
consumed = true;
}
ss_prev = ss;
// F7 — Canviar tipus de shader (PostFX ↔ CrtPi)
bool next_shader = JI_KeyPressed(KeyConfig::scancode("next_shader"));
if (next_shader && !next_shader_prev) {
});
consumed |= edgeTrigger("next_shader", next_shader_prev, [] {
if (Screen::get()->nextShaderType()) {
char msg[64];
snprintf(msg, sizeof(msg), "%s: %s", Screen::get()->getActiveShaderName(), Screen::get()->getCurrentPresetName());
Overlay::showNotification(msg);
}
}
if (next_shader) {
consumed = true;
}
next_shader_prev = next_shader;
// F8 — Pròxim preset del shader actiu
bool next_preset = JI_KeyPressed(KeyConfig::scancode("next_shader_preset"));
if (next_preset && !next_preset_prev) {
});
consumed |= edgeTrigger("next_shader_preset", next_preset_prev, [] {
if (Screen::get()->nextPreset()) {
char msg[64];
snprintf(msg, sizeof(msg), Locale::get("notifications.preset_fmt"), Screen::get()->getCurrentPresetName());
Overlay::showNotification(msg);
}
}
if (next_preset) {
consumed = true;
}
next_preset_prev = next_preset;
// F9 — Cicla filtre de textura (NEAREST ↔ LINEAR), sempre aplicat
bool texture_filter = JI_KeyPressed(KeyConfig::scancode("cycle_texture_filter"));
if (texture_filter && !texture_filter_prev) {
});
consumed |= edgeTrigger("cycle_texture_filter", texture_filter_prev, [] {
Screen::get()->cycleTextureFilter(+1);
Overlay::showNotification(Options::video.texture_filter == Options::TextureFilter::LINEAR
? Locale::get("notifications.filter_linear")
: Locale::get("notifications.filter_nearest"));
}
if (texture_filter) {
consumed = true;
}
texture_filter_prev = texture_filter;
// F10 — Toggle render info (FPS, driver, shader)
bool render_info = JI_KeyPressed(KeyConfig::scancode("toggle_render_info"));
if (render_info && !render_info_prev) {
});
consumed |= edgeTrigger("toggle_render_info", render_info_prev, [] {
Overlay::toggleRenderInfo();
}
if (render_info) {
consumed = true;
}
render_info_prev = render_info;
});
return consumed;
}
+1 -1
View File
@@ -1,7 +1,7 @@
#pragma once
namespace GlobalInputs {
// Comprovar una vegada per frame, després de JI_Update()
// Comprovar una vegada per frame, després de Ji::update()
// Retorna true si ha consumit alguna tecla (per suprimir-la de la capa de joc)
auto handle() -> bool;
} // namespace GlobalInputs
+28 -28
View File
@@ -11,13 +11,13 @@
namespace KeyConfig {
namespace {
std::vector<KeyEntry> entries_;
std::unordered_map<std::string, size_t> index_;
std::string overrides_path_;
std::vector<KeyEntry> key_entries;
std::unordered_map<std::string, size_t> index_table;
std::string overrides_path;
auto findIndex(const std::string& id) -> size_t {
auto it = index_.find(id);
if (it == index_.end()) {
auto it = index_table.find(id);
if (it == index_table.end()) {
return SIZE_MAX;
}
return it->second;
@@ -52,10 +52,10 @@ namespace KeyConfig {
entry.scancode = sc;
entry.default_scancode = sc;
index_[entry.id] = entries_.size();
entries_.push_back(std::move(entry));
index_table[entry.id] = key_entries.size();
key_entries.push_back(std::move(entry));
}
std::cout << "KeyConfig: " << entries_.size() << " tecles carregades de "
std::cout << "KeyConfig: " << key_entries.size() << " tecles carregades de "
<< defaults_resource_path << '\n';
} catch (const fkyaml::exception& e) {
std::cerr << "KeyConfig: error parsejant YAML: " << e.what() << '\n';
@@ -93,8 +93,8 @@ namespace KeyConfig {
<< "' per '" << id << "'\n";
continue;
}
entries_[idx].scancode = sc;
entries_[idx].code = code;
key_entries[idx].scancode = sc;
key_entries[idx].code = code;
applied++;
}
if (applied > 0) {
@@ -109,20 +109,20 @@ namespace KeyConfig {
void init(const std::string& defaults_resource_path,
const std::string& user_overrides_disk_path) {
entries_.clear();
index_.clear();
overrides_path_ = user_overrides_disk_path;
key_entries.clear();
index_table.clear();
overrides_path = user_overrides_disk_path;
loadDefaults(defaults_resource_path);
if (!overrides_path_.empty()) {
applyOverrides(overrides_path_);
if (!overrides_path.empty()) {
applyOverrides(overrides_path);
}
}
void destroy() {
entries_.clear();
index_.clear();
overrides_path_.clear();
key_entries.clear();
index_table.clear();
overrides_path.clear();
}
auto scancode(const std::string& id) -> SDL_Scancode {
@@ -130,7 +130,7 @@ namespace KeyConfig {
if (idx == SIZE_MAX) {
return SDL_SCANCODE_UNKNOWN;
}
return entries_[idx].scancode;
return key_entries[idx].scancode;
}
auto scancodePtr(const std::string& id) -> SDL_Scancode* {
@@ -138,7 +138,7 @@ namespace KeyConfig {
if (idx == SIZE_MAX) {
return nullptr;
}
return &entries_[idx].scancode;
return &key_entries[idx].scancode;
}
void setScancode(const std::string& id, SDL_Scancode sc) {
@@ -146,38 +146,38 @@ namespace KeyConfig {
if (idx == SIZE_MAX) {
return;
}
entries_[idx].scancode = sc;
key_entries[idx].scancode = sc;
const char* name = SDL_GetScancodeName(sc);
entries_[idx].code = (name != nullptr) ? name : "";
key_entries[idx].code = (name != nullptr) ? name : "";
}
auto isGuiKey(SDL_Scancode sc) -> bool {
if (sc == SDL_SCANCODE_UNKNOWN) {
return false;
}
return std::ranges::any_of(entries_, [sc](const auto& e) { return e.scancode == sc; });
return std::ranges::any_of(key_entries, [sc](const auto& e) { return e.scancode == sc; });
}
auto entries() -> const std::vector<KeyEntry>& {
return entries_;
return key_entries;
}
auto saveOverrides() -> bool {
if (overrides_path_.empty()) {
if (overrides_path.empty()) {
return false;
}
// Recull només les entrades remapeades.
std::vector<const KeyEntry*> changed;
for (const auto& e : entries_) {
for (const auto& e : key_entries) {
if (e.scancode != e.default_scancode) {
changed.push_back(&e);
}
}
std::ofstream file(overrides_path_);
std::ofstream file(overrides_path);
if (!file.is_open()) {
std::cerr << "KeyConfig: no es pot escriure " << overrides_path_ << '\n';
std::cerr << "KeyConfig: no es pot escriure " << overrides_path << '\n';
return false;
}
+2 -2
View File
@@ -9,10 +9,10 @@ namespace KeyRemap {
static void mirror(SDL_Scancode custom, SDL_Scancode standard, const bool* ks) {
if (custom == standard || custom == SDL_SCANCODE_UNKNOWN) {
JI_SetVirtualKey(standard, JI_VSRC_REMAP, false);
Ji::setVirtualKey(standard, Ji::VirtualSource::REMAP, false);
return;
}
JI_SetVirtualKey(standard, JI_VSRC_REMAP, ks[custom]);
Ji::setVirtualKey(standard, Ji::VirtualSource::REMAP, ks[custom]);
}
void update() {
+55 -55
View File
@@ -21,17 +21,17 @@
#pragma GCC diagnostic pop
#endif
JD8_Surface screen = nullptr;
JD8_Palette main_palette = nullptr;
Jd8::Surface screen = nullptr;
Jd8::Palette main_palette = nullptr;
Uint32* pixel_data = nullptr;
void JD8_Init() {
void Jd8::init() {
screen = new Uint8[64000]{};
main_palette = new Color[256]{};
pixel_data = new Uint32[std::size_t{320} * 200]{};
}
void JD8_Quit() {
void Jd8::quit() {
delete[] screen;
delete[] main_palette;
delete[] pixel_data;
@@ -40,30 +40,30 @@ void JD8_Quit() {
pixel_data = nullptr;
}
void JD8_ClearScreen(Uint8 color) {
void Jd8::clearScreen(Uint8 color) {
memset(screen, color, 64000);
}
auto JD8_NewSurface() -> JD8_Surface {
auto Jd8::newSurface() -> Jd8::Surface {
return new Uint8[64000]{};
}
// Helper intern: deriva el basename d'una ruta per a buscar al Cache.
static auto jd8_basename(const char* file) -> std::string {
static auto pathBasename(const char* file) -> std::string {
std::string s = file;
auto pos = s.find_last_of("/\\");
return pos == std::string::npos ? s : s.substr(pos + 1);
}
auto JD8_LoadSurface(const char* file) -> JD8_Surface {
auto Jd8::loadSurface(const char* file) -> Jd8::Surface {
// Prova primer el Resource::Cache. Si l'asset és precarregat, copiem
// els 64KB des del cache (microsegons) i ens estalviem la decodificació
// GIF. Mantenim el contracte de la funció: el caller rep un buffer
// fresc que ha d'alliberar amb JD8_FreeSurface.
// fresc que ha d'alliberar amb Jd8::freeSurface.
if (Resource::Cache::get() != nullptr) {
try {
const auto& cached = Resource::Cache::get()->getSurfacePixels(jd8_basename(file));
JD8_Surface image = JD8_NewSurface();
const auto& cached = Resource::Cache::get()->getSurfacePixels(pathBasename(file));
Jd8::Surface image = Jd8::newSurface();
memcpy(image, cached.data(), 64000);
return image;
} catch (const std::exception&) {
@@ -79,21 +79,21 @@ auto JD8_LoadSurface(const char* file) -> JD8_Surface {
printf("Unable to load bitmap: %s\n", SDL_GetError());
exit(1);
}
JD8_Surface image = JD8_NewSurface();
Jd8::Surface image = Jd8::newSurface();
memcpy(image, pixels, 64000);
free(pixels);
return image;
}
auto JD8_LoadPalette(const char* file) -> JD8_Palette {
auto Jd8::loadPalette(const char* file) -> Jd8::Palette {
// Sempre retorna un buffer de 256 colors reservat amb `new Color[256]`
// — el caller és responsable d'alliberar-lo amb `delete[]` (o lliurar-ne
// l'ownership a `JD8_SetScreenPalette`).
// l'ownership a `Jd8::setScreenPalette`).
auto* palette = new Color[256];
if (Resource::Cache::get() != nullptr) {
try {
const auto& cached = Resource::Cache::get()->getPaletteBytes(jd8_basename(file));
const auto& cached = Resource::Cache::get()->getPaletteBytes(pathBasename(file));
memcpy(palette, cached.data(), 768);
return palette;
} catch (const std::exception&) {
@@ -108,7 +108,7 @@ auto JD8_LoadPalette(const char* file) -> JD8_Palette {
return palette;
}
void JD8_SetScreenPalette(JD8_Palette palette) {
void Jd8::setScreenPalette(Jd8::Palette palette) {
if (main_palette == palette) {
return;
}
@@ -116,13 +116,13 @@ void JD8_SetScreenPalette(JD8_Palette palette) {
main_palette = palette;
}
void JD8_FillSquare(int ini, int height, Uint8 color) {
const int offset = ini * 320;
const int size = height * 320;
memset(&screen[offset], color, size);
void Jd8::fillSquare(int ini, int height, Uint8 color) {
const int OFFSET = ini * 320;
const int SIZE = height * 320;
memset(&screen[OFFSET], color, SIZE);
}
void JD8_FillRect(int x, int y, int w, int h, Uint8 color) {
void Jd8::fillRect(int x, int y, int w, int h, Uint8 color) {
if (x < 0) {
w += x;
x = 0;
@@ -145,11 +145,11 @@ void JD8_FillRect(int x, int y, int w, int h, Uint8 color) {
}
}
void JD8_Blit(const Uint8* surface) {
void Jd8::blit(const Uint8* surface) {
memcpy(screen, surface, 64000);
}
void JD8_Blit(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh) {
void Jd8::blit(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh) {
int src_pointer = sx + (sy * 320);
int dst_pointer = x + (y * 320);
for (int i = 0; i < sh; i++) {
@@ -159,7 +159,7 @@ void JD8_Blit(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh
}
}
void JD8_BlitToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, JD8_Surface dest) {
void Jd8::blitToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Jd8::Surface dest) {
int src_pointer = sx + (sy * 320);
int dst_pointer = x + (y * 320);
for (int i = 0; i < sh; i++) {
@@ -169,7 +169,7 @@ void JD8_BlitToSurface(int x, int y, const Uint8* surface, int sx, int sy, int s
}
}
void JD8_BlitCK(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
void Jd8::blitCK(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
int src_pointer = sx + (sy * 320);
int dst_pointer = x + (y * 320);
for (int j = 0; j < sh; j++) {
@@ -183,7 +183,7 @@ void JD8_BlitCK(int x, int y, const Uint8* surface, int sx, int sy, int sw, int
}
}
void JD8_BlitCKCut(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
void Jd8::blitCKCut(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
int src_pointer = sx + (sy * 320);
int dst_pointer = x + (y * 320);
for (int j = 0; j < sh; j++) {
@@ -197,7 +197,7 @@ void JD8_BlitCKCut(int x, int y, const Uint8* surface, int sx, int sy, int sw, i
}
}
void JD8_BlitCKScroll(int y, const Uint8* surface, int sx, int sy, int sh, Uint8 colorkey) {
void Jd8::blitCKScroll(int y, const Uint8* surface, int sx, int sy, int sh, Uint8 colorkey) {
int dst_pointer = y * 320;
for (int j = sy; j < sy + sh; j++) {
for (int i = 0; i < 320; i++) {
@@ -210,7 +210,7 @@ void JD8_BlitCKScroll(int y, const Uint8* surface, int sx, int sy, int sh, Uint8
}
}
void JD8_BlitCKToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, JD8_Surface dest, Uint8 colorkey) {
void Jd8::blitCKToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Jd8::Surface dest, Uint8 colorkey) {
int src_pointer = sx + (sy * 320);
int dst_pointer = x + (y * 320);
for (int j = 0; j < sh; j++) {
@@ -224,7 +224,7 @@ void JD8_BlitCKToSurface(int x, int y, const Uint8* surface, int sx, int sy, int
}
}
void JD8_Flip() {
void Jd8::flip() {
// Converteix el framebuffer indexat (paletted) a ARGB (pixel_data).
// El Director crida aquesta funció després del tick de cada escena
// per preparar el frame abans de presentar-lo. Ja no fa yield —
@@ -237,23 +237,23 @@ void JD8_Flip() {
}
}
auto JD8_GetFramebuffer() -> Uint32* {
auto Jd8::getFramebuffer() -> Uint32* {
return pixel_data;
}
void JD8_FreeSurface(JD8_Surface surface) { // NOLINT(readability-non-const-parameter): allibera memòria, no pot ser const
void Jd8::freeSurface(Jd8::Surface surface) { // NOLINT(readability-non-const-parameter): allibera memòria, no pot ser const
delete[] surface;
}
auto JD8_GetPixel(const Uint8* surface, int x, int y) -> Uint8 {
auto Jd8::getPixel(const Uint8* surface, int x, int y) -> Uint8 {
return surface[x + (y * 320)];
}
void JD8_PutPixel(JD8_Surface surface, int x, int y, Uint8 pixel) {
void Jd8::putPixel(Jd8::Surface surface, int x, int y, Uint8 pixel) {
surface[x + (y * 320)] = pixel;
}
void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b) {
void Jd8::setPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b) {
main_palette[index].r = r << 2;
main_palette[index].g = g << 2;
main_palette[index].b = b << 2;
@@ -265,25 +265,25 @@ void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b) {
namespace {
enum class FadeType : std::uint8_t {
None = 0,
Out,
ToPal,
NONE = 0,
OUT,
TO_PAL,
};
constexpr int FADE_STEPS = 32;
FadeType fade_type = FadeType::None;
FadeType fade_type = FadeType::NONE;
Color fade_target[256];
int fade_step = 0;
void apply_fade_step() {
if (fade_type == FadeType::Out) {
void applyFadeStep() {
if (fade_type == FadeType::OUT) {
for (int i = 0; i < 256; i++) {
main_palette[i].r = main_palette[i].r >= 8 ? main_palette[i].r - 8 : 0;
main_palette[i].g = main_palette[i].g >= 8 ? main_palette[i].g - 8 : 0;
main_palette[i].b = main_palette[i].b >= 8 ? main_palette[i].b - 8 : 0;
}
} else if (fade_type == FadeType::ToPal) {
} else if (fade_type == FadeType::TO_PAL) {
for (int i = 0; i < 256; i++) {
main_palette[i].r = main_palette[i].r <= int(fade_target[i].r) - 8
? main_palette[i].r + 8
@@ -300,39 +300,39 @@ namespace {
} // namespace
void JD8_FadeStartOut() {
fade_type = FadeType::Out;
void Jd8::fadeStartOut() {
fade_type = FadeType::OUT;
fade_step = 0;
}
void JD8_FadeStartToPal(const Color* pal) {
fade_type = FadeType::ToPal;
void Jd8::fadeStartToPal(const Color* pal) {
fade_type = FadeType::TO_PAL;
memcpy(fade_target, pal, sizeof(Color) * 256);
fade_step = 0;
}
auto JD8_FadeIsActive() -> bool {
return fade_type != FadeType::None;
auto Jd8::fadeIsActive() -> bool {
return fade_type != FadeType::NONE;
}
auto JD8_FadeTickStep() -> bool {
if (fade_type == FadeType::None) {
auto Jd8::fadeTickStep() -> bool {
if (fade_type == FadeType::NONE) {
return true;
}
apply_fade_step();
applyFadeStep();
fade_step++;
if (fade_step >= FADE_STEPS) {
fade_type = FadeType::None;
fade_type = FadeType::NONE;
return true;
}
return false;
}
// Els shims bloquejants `JD8_FadeOut` i `JD8_FadeToPal` han estat
// eliminats a Phase B.2: feien un bucle de 32 iteracions amb `JD8_Flip`
// eliminats a Phase B.2: feien un bucle de 32 iteracions amb `Jd8::flip`
// entre cada una que només funcionava mentre l'entorn tenia fibers i
// `JD8_Flip` cedia el control al Director. Ara tot fade es fa tick a
// tick via `scenes::PaletteFade` (que encapsula `JD8_FadeStartOut` /
// `JD8_FadeStartToPal` + `JD8_FadeTickStep`).
// `Jd8::flip` cedia el control al Director. Ara tot fade es fa tick a
// tick via `Scenes::PaletteFade` (que encapsula `Jd8::fadeStartOut` /
// `Jd8::fadeStartToPal` + `Jd8::fadeTickStep`).
+78 -80
View File
@@ -1,80 +1,78 @@
#pragma once
#include <SDL3/SDL.h>
struct Color {
Uint8 r;
Uint8 g;
Uint8 b;
};
using JD8_Surface = Uint8*;
using JD8_Palette = Color*;
void JD8_Init();
void JD8_Quit();
void JD8_ClearScreen(Uint8 color);
auto JD8_NewSurface() -> JD8_Surface;
auto JD8_LoadSurface(const char* file) -> JD8_Surface;
auto JD8_LoadPalette(const char* file) -> JD8_Palette;
void JD8_SetScreenPalette(JD8_Palette palette);
void JD8_FillSquare(int ini, int height, Uint8 color);
// Omple un rectangle arbitrari de la pantalla amb un color paletat.
// Pensat per a UI senzilla (barra de progrés del BootLoader, etc.).
void JD8_FillRect(int x, int y, int w, int h, Uint8 color);
void JD8_Blit(const Uint8* surface);
void JD8_Blit(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh);
void JD8_BlitToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, JD8_Surface dest);
void JD8_BlitCK(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
void JD8_BlitCKCut(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
void JD8_BlitCKScroll(int y, const Uint8* surface, int sx, int sy, int sh, Uint8 colorkey);
void JD8_BlitCKToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, JD8_Surface dest, Uint8 colorkey);
// Converteix la pantalla indexada a ARGB. El Director crida aquesta
// funció al final de cada tick i després llegeix el framebuffer via
// JD8_GetFramebuffer() per presentar-lo.
void JD8_Flip();
// Accés al framebuffer ARGB de 320x200 actualitzat per l'última crida a
// JD8_Flip(). Propietat de jdraw8 — el caller no ha de lliberar-lo.
auto JD8_GetFramebuffer() -> Uint32*;
void JD8_FreeSurface(JD8_Surface surface);
auto JD8_GetPixel(const Uint8* surface, int x, int y) -> Uint8;
void JD8_PutPixel(JD8_Surface surface, int x, int y, Uint8 pixel);
void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b);
// API de fade no bloquejant (màquina d'estats). `FadeStart*` inicia el
// fade; `FadeTickStep` aplica un pas i retorna `true` quan el fade ha
// acabat. Un pas correspon visualment a una iteració del fade original
// (32 passos en total). El caller és responsable de fer el Flip entre
// passos si el vol veure animat. `FadeIsActive` permet saber si hi ha
// un fade en curs per a enllaçar-lo amb un altre subsistema.
// L'embolcall `scenes::PaletteFade` ho fa més idiomàtic per a escenes.
void JD8_FadeStartOut();
void JD8_FadeStartToPal(const Color* pal);
auto JD8_FadeTickStep() -> bool;
auto JD8_FadeIsActive() -> bool;
// JD_Font JD_LoadFont( char *file, int width, int height);
// void JD_DrawText( int x, int y, JD_Font *source, char *text);
// char *JD_GetFPS();
#pragma once
#include <SDL3/SDL.h>
struct Color {
Uint8 r;
Uint8 g;
Uint8 b;
};
namespace Jd8 {
using Surface = Uint8*;
using Palette = Color*;
void init();
void quit();
void clearScreen(Uint8 color);
auto newSurface() -> Surface;
auto loadSurface(const char* file) -> Surface;
auto loadPalette(const char* file) -> Palette;
void setScreenPalette(Palette palette);
void fillSquare(int ini, int height, Uint8 color);
// Omple un rectangle arbitrari de la pantalla amb un color paletat.
// Pensat per a UI senzilla (barra de progrés del BootLoader, etc.).
void fillRect(int x, int y, int w, int h, Uint8 color);
void blit(const Uint8* surface);
void blit(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh);
void blitToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Surface dest);
void blitCK(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
void blitCKCut(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
void blitCKScroll(int y, const Uint8* surface, int sx, int sy, int sh, Uint8 colorkey);
void blitCKToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Surface dest, Uint8 colorkey);
// Converteix la pantalla indexada a ARGB. El Director crida aquesta
// funció al final de cada tick i després llegeix el framebuffer via
// getFramebuffer() per presentar-lo.
void flip();
// Accés al framebuffer ARGB de 320x200 actualitzat per l'última crida a
// flip(). Propietat de jdraw8 — el caller no ha de lliberar-lo.
auto getFramebuffer() -> Uint32*;
void freeSurface(Surface surface);
auto getPixel(const Uint8* surface, int x, int y) -> Uint8;
void putPixel(Surface surface, int x, int y, Uint8 pixel);
void setPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b);
// API de fade no bloquejant (màquina d'estats). `fadeStart*` inicia el
// fade; `fadeTickStep` aplica un pas i retorna `true` quan el fade ha
// acabat. Un pas correspon visualment a una iteració del fade original
// (32 passos en total). El caller és responsable de fer el Flip entre
// passos si el vol veure animat. `fadeIsActive` permet saber si hi ha
// un fade en curs per a enllaçar-lo amb un altre subsistema.
// L'embolcall `Scenes::PaletteFade` ho fa més idiomàtic per a escenes.
void fadeStartOut();
void fadeStartToPal(const Color* pal);
auto fadeTickStep() -> bool;
auto fadeIsActive() -> bool;
} // namespace Jd8
+32 -32
View File
@@ -15,36 +15,36 @@
namespace {
struct keyvalue {
struct Keyvalue {
std::string key;
std::string value;
};
std::vector<keyvalue> config;
std::vector<Keyvalue> config;
std::string resource_folder;
std::string config_folder;
void load_config_values() {
void loadConfigValues() {
config.clear();
const std::string config_file = config_folder + "/config.txt";
std::ifstream fi(config_file);
const std::string CONFIG_FILE = config_folder + "/config.txt";
std::ifstream fi(CONFIG_FILE);
if (!fi.is_open()) {
return;
}
std::string line;
while (std::getline(fi, line)) {
const auto eq = line.find('=');
if (eq == std::string::npos) {
const auto EQ = line.find('=');
if (EQ == std::string::npos) {
continue;
}
config.push_back({line.substr(0, eq), line.substr(eq + 1)});
config.push_back({line.substr(0, EQ), line.substr(EQ + 1)});
}
}
void save_config_values() {
const std::string config_file = config_folder + "/config.txt";
std::ofstream fo(config_file);
void saveConfigValues() {
const std::string CONFIG_FILE = config_folder + "/config.txt";
std::ofstream fo(CONFIG_FILE);
if (!fo.is_open()) {
return;
}
@@ -55,17 +55,17 @@ namespace {
} // namespace
void file_setresourcefolder(const char* str) {
void Jf::setResourceFolder(const char* str) {
resource_folder = str;
}
auto file_getresourcefolder() -> const char* {
auto Jf::getResourceFolder() -> const char* {
return resource_folder.c_str();
}
// Crea la carpeta del sistema on guardar les dades.
// Accepta rutes amb subdirectoris (ex: "jailgames/aee") i crea tota la jerarquia.
void file_setconfigfolder(const char* foldername) {
void Jf::setConfigFolder(const char* foldername) {
#ifdef _WIN32
const char* base = getenv("APPDATA");
if (!base) base = "C:/";
@@ -102,35 +102,35 @@ void file_setconfigfolder(const char* foldername) {
// volàtil al navegador de totes formes: ignorem l'error i continuem.
}
auto file_getconfigfolder() -> const char* {
thread_local std::string folder;
folder = config_folder + "/";
return folder.c_str();
auto Jf::getConfigFolder() -> const char* {
thread_local std::string folder_;
folder_ = config_folder + "/";
return folder_.c_str();
}
auto file_getconfigvalue(const char* key) -> const char* {
auto Jf::getConfigValue(const char* key) -> const char* {
if (config.empty()) {
load_config_values();
loadConfigValues();
}
const auto it = std::find_if(config.begin(), config.end(), [key](const keyvalue& pair) { return pair.key == key; });
if (it != config.end()) {
thread_local std::string value_cache;
value_cache = it->value;
return value_cache.c_str();
const auto IT = std::ranges::find_if(config, [key](const Keyvalue& pair) { return pair.key == key; });
if (IT != config.end()) {
thread_local std::string value_cache_;
value_cache_ = IT->value;
return value_cache_.c_str();
}
return nullptr;
}
void file_setconfigvalue(const char* key, const char* value) {
void Jf::setConfigValue(const char* key, const char* value) {
if (config.empty()) {
load_config_values();
loadConfigValues();
}
const auto it = std::find_if(config.begin(), config.end(), [key](const keyvalue& pair) { return pair.key == key; });
if (it != config.end()) {
it->value = value;
save_config_values();
const auto IT = std::ranges::find_if(config, [key](const Keyvalue& pair) { return pair.key == key; });
if (IT != config.end()) {
IT->value = value;
saveConfigValues();
return;
}
config.push_back({std::string(key), std::string(value)});
save_config_values();
saveConfigValues();
}
+10 -6
View File
@@ -1,10 +1,14 @@
#pragma once
void file_setconfigfolder(const char* foldername);
auto file_getconfigfolder() -> const char*;
namespace Jf {
void file_setresourcefolder(const char* str);
auto file_getresourcefolder() -> const char*;
void setConfigFolder(const char* foldername);
auto getConfigFolder() -> const char*;
auto file_getconfigvalue(const char* key) -> const char*;
void file_setconfigvalue(const char* key, const char* value);
void setResourceFolder(const char* str);
auto getResourceFolder() -> const char*;
auto getConfigValue(const char* key) -> const char*;
void setConfigValue(const char* key, const char* value);
} // namespace Jf
+19 -19
View File
@@ -2,7 +2,7 @@
namespace {
bool quitting = false;
bool is_quitting = false;
Uint32 update_ticks = 0;
Uint32 update_time = 0;
Uint32 cycle_counter = 0;
@@ -10,48 +10,48 @@ namespace {
} // namespace
void JG_Init() {
void Jg::init() {
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
update_time = SDL_GetTicks();
last_delta_time = update_time;
}
void JG_Finalize() {
void Jg::finalize() {
SDL_Quit();
}
void JG_QuitSignal() {
quitting = true;
void Jg::quitSignal() {
is_quitting = true;
}
auto JG_Quitting() -> bool {
return quitting;
auto Jg::quitting() -> bool {
return is_quitting;
}
void JG_SetUpdateTicks(Uint32 milliseconds) {
void Jg::setUpdateTicks(Uint32 milliseconds) {
update_ticks = milliseconds;
}
auto JG_ShouldUpdate() -> bool {
const Uint32 now = SDL_GetTicks();
if (now - update_time > update_ticks) {
update_time = now;
auto Jg::shouldUpdate() -> bool {
const Uint32 NOW = SDL_GetTicks();
if (NOW - update_time > update_ticks) {
update_time = NOW;
cycle_counter++;
return true;
}
// No toca update — retornem false sense més. Des de Phase B.2 ja no
// hi ha fibers: cap caller fa spin-waits (`while (!JG_ShouldUpdate())`)
// hi ha fibers: cap caller fa spin-waits (`while (!Jg::shouldUpdate())`)
// i el Director pren el control del main loop frame a frame.
return false;
}
auto JG_GetCycleCounter() -> Uint32 {
auto Jg::getCycleCounter() -> Uint32 {
return cycle_counter;
}
auto JG_GetDeltaMs() -> Uint32 {
const Uint32 now = SDL_GetTicks();
const Uint32 delta = now - last_delta_time;
last_delta_time = now;
return delta;
auto Jg::getDeltaMs() -> Uint32 {
const Uint32 NOW = SDL_GetTicks();
const Uint32 DELTA = NOW - last_delta_time;
last_delta_time = NOW;
return DELTA;
}
+24 -20
View File
@@ -1,20 +1,24 @@
#pragma once
#include <SDL3/SDL.h>
void JG_Init();
void JG_Finalize();
void JG_QuitSignal();
auto JG_Quitting() -> bool;
void JG_SetUpdateTicks(Uint32 milliseconds);
auto JG_ShouldUpdate() -> bool;
auto JG_GetCycleCounter() -> Uint32;
// Temps transcorregut (en ms) des de l'última crida a JG_GetDeltaMs.
// Helper per a la migració progressiva a time-based (Fase 4+).
auto JG_GetDeltaMs() -> Uint32;
#pragma once
#include <SDL3/SDL.h>
namespace Jg {
void init();
void finalize();
void quitSignal();
auto quitting() -> bool;
void setUpdateTicks(Uint32 milliseconds);
auto shouldUpdate() -> bool;
auto getCycleCounter() -> Uint32;
// Temps transcorregut (en ms) des de l'última crida a Jg::getDeltaMs.
// Helper per a la migració progressiva a time-based (Fase 4+).
auto getDeltaMs() -> Uint32;
} // namespace Jg
+26 -25
View File
@@ -17,20 +17,20 @@ namespace {
bool key_pressed = false;
// Temps restant en mil·lisegons durant el qual JI_KeyPressed/JI_AnyKey
// Temps restant en mil·lisegons durant el qual Ji::keyPressed/Ji::anyKey
// retornen false. Utilitzat per a evitar que pulsacions fortuïtes
// saltin cinemàtiques al començament.
float wait_ms = 0.0F;
// Per a calcular el delta entre crides a JI_Update sense que els callers
// Per a calcular el delta entre crides a Ji::update sense que els callers
// hagen de passar-lo explícitament. Es reinicia a la primera crida.
Uint64 last_update_tick = 0;
bool input_blocked = false;
Uint8 virtual_keystates[JI_VSRC_COUNT][SDL_SCANCODE_COUNT] = {{0}};
Uint8 virtual_keystates[static_cast<size_t>(Ji::VirtualSource::COUNT)][SDL_SCANCODE_COUNT] = {{0}};
auto scancode_to_ascii(Uint8 scancode) -> Uint8 {
auto scancodeToAscii(Uint8 scancode) -> Uint8 {
if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) {
return static_cast<Uint8>('a' + (scancode - SDL_SCANCODE_A));
}
@@ -39,48 +39,49 @@ namespace {
} // namespace
void JI_DisableKeyboard(Uint32 time) {
void Ji::disableKeyboard(Uint32 time) {
wait_ms = static_cast<float>(time);
}
void JI_SetInputBlocked(bool blocked) {
void Ji::setInputBlocked(bool blocked) {
input_blocked = blocked;
}
void JI_SetVirtualKey(int scancode, int source, bool pressed) {
void Ji::setVirtualKey(int scancode, VirtualSource source, bool pressed) {
if (scancode < 0 || scancode >= SDL_SCANCODE_COUNT) {
return;
}
if (source < 0 || source >= JI_VSRC_COUNT) {
const auto SRC_IDX = static_cast<size_t>(source);
if (SRC_IDX >= static_cast<size_t>(VirtualSource::COUNT)) {
return;
}
virtual_keystates[source][scancode] = pressed ? 1 : 0;
virtual_keystates[SRC_IDX][scancode] = pressed ? 1 : 0;
}
void JI_moveCheats(Uint8 scancode) {
void Ji::moveCheats(Uint8 scancode) {
cheat[0] = cheat[1];
cheat[1] = cheat[2];
cheat[2] = cheat[3];
cheat[3] = cheat[4];
cheat[4] = scancode_to_ascii(scancode);
cheat[4] = scancodeToAscii(scancode);
}
void JI_Update() {
void Ji::update() {
// El director ha processat tots els events. Ací només refresquem
// el snapshot del teclat i consumim el flag de tecla polsada.
if (keystates == nullptr) {
keystates = SDL_GetKeyboardState(nullptr);
}
const Uint64 now = SDL_GetTicks();
const Uint64 NOW = SDL_GetTicks();
if (last_update_tick == 0) {
last_update_tick = now;
last_update_tick = NOW;
}
const auto delta_ms = static_cast<float>(now - last_update_tick);
last_update_tick = now;
const auto DELTA_MS = static_cast<float>(NOW - last_update_tick);
last_update_tick = NOW;
if (wait_ms > 0.0F) {
wait_ms -= delta_ms;
wait_ms -= DELTA_MS;
wait_ms = std::max(wait_ms, 0.0F);
}
@@ -88,7 +89,7 @@ void JI_Update() {
key_pressed = Director::get()->consumeKeyPressed();
}
auto JI_KeyPressed(int key) -> bool {
auto Ji::keyPressed(int key) -> bool {
if (wait_ms > 0.0F || keystates == nullptr) {
return false;
}
@@ -109,22 +110,22 @@ auto JI_KeyPressed(int key) -> bool {
return std::ranges::any_of(virtual_keystates, [key](const auto& vk) { return vk[key] != 0; });
}
auto JI_CheatActivated(const char* cheat_code) -> bool {
const size_t len = std::strlen(cheat_code);
if (len > sizeof(cheat)) {
auto Ji::cheatActivated(const char* cheat_code) -> bool {
const size_t LEN = std::strlen(cheat_code);
if (LEN > sizeof(cheat)) {
return false;
}
// Compara contra els últims `len` caràcters del buffer. El buffer té
// mida fixa 5 i acumula sempre el darrer tecle a la posició 4.
const size_t offset = sizeof(cheat) - len;
for (size_t i = 0; i < len; i++) {
if (cheat[offset + i] != static_cast<Uint8>(cheat_code[i])) {
const size_t OFFSET = sizeof(cheat) - LEN;
for (size_t i = 0; i < LEN; i++) {
if (cheat[OFFSET + i] != static_cast<Uint8>(cheat_code[i])) {
return false;
}
}
return true;
}
auto JI_AnyKey() -> bool {
auto Ji::anyKey() -> bool {
return wait_ms > 0.0F ? false : key_pressed;
}
+36 -27
View File
@@ -1,27 +1,36 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
void JI_DisableKeyboard(Uint32 time);
// Bloqueja tot l'input cap al joc (JI_KeyPressed retorna false per a tot)
void JI_SetInputBlocked(bool blocked);
// Estableix l'estat d'una tecla virtual. Múltiples fonts (gamepad, remap)
// s'agrupen per OR. JI_KeyPressed retorna true si el teclat real O qualsevol
// font virtual està premuda.
enum JI_VirtualSource : std::uint8_t {
JI_VSRC_GAMEPAD = 0,
JI_VSRC_REMAP = 1,
JI_VSRC_COUNT = 2
};
void JI_SetVirtualKey(int scancode, int source, bool pressed);
void JI_Update();
auto JI_KeyPressed(int key) -> bool;
auto JI_CheatActivated(const char* cheat_code) -> bool;
auto JI_AnyKey() -> bool;
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
namespace Ji {
void disableKeyboard(Uint32 time);
// Bloqueja tot l'input cap al joc (Ji::keyPressed retorna false per a tot)
void setInputBlocked(bool blocked);
// Estableix l'estat d'una tecla virtual. Múltiples fonts (gamepad, remap)
// s'agrupen per OR. Ji::keyPressed retorna true si el teclat real O qualsevol
// font virtual està premuda.
enum class VirtualSource : std::uint8_t {
GAMEPAD = 0,
REMAP = 1,
COUNT = 2
};
void setVirtualKey(int scancode, VirtualSource source, bool pressed);
void update();
// Avança el buffer rotatori de cheats afegint `scancode` per detectar
// seqüències com "reviu", "alone", "obert". Usat pel Director quan rep
// un KEY_DOWN; el joc no l'ha de cridar directament.
void moveCheats(Uint8 scancode);
auto keyPressed(int key) -> bool;
auto cheatActivated(const char* cheat_code) -> bool;
auto anyKey() -> bool;
} // namespace Ji
+6 -6
View File
@@ -9,7 +9,7 @@
namespace Locale {
static std::unordered_map<std::string, std::string> strings_;
static std::unordered_map<std::string, std::string> strings_table;
// Aplana un node YAML en claus amb notació punt
static void traverse(const fkyaml::node& node, const std::string& prefix) {
@@ -25,7 +25,7 @@ namespace Locale {
}
} else if (node.is_scalar()) {
try {
strings_[prefix] = node.get_value<std::string>();
strings_table[prefix] = node.get_value<std::string>();
} catch (...) {
// @INTENTIONAL: si el valor no és string vàlid, l'ignorem i continuem.
}
@@ -42,9 +42,9 @@ namespace Locale {
try {
auto yaml = fkyaml::node::deserialize(content);
strings_.clear();
strings_table.clear();
traverse(yaml, "");
std::cout << "Locale loaded: " << strings_.size() << " string(s) from " << filename << '\n';
std::cout << "Locale loaded: " << strings_table.size() << " string(s) from " << filename << '\n';
return true;
} catch (const fkyaml::exception& e) {
std::cerr << "Locale: error parsing " << filename << ": " << e.what() << '\n';
@@ -53,8 +53,8 @@ namespace Locale {
}
auto get(const char* key) -> const char* {
auto it = strings_.find(key);
if (it != strings_.end()) {
auto it = strings_table.find(key);
if (it != strings_table.end()) {
return it->second.c_str();
}
return key; // fallback: retorna la clau mateixa
+283 -257
View File
@@ -49,21 +49,21 @@ namespace Menu {
static constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada (~150 ms al 90%)
// --- Items ---
enum class ItemKind : std::uint8_t { Toggle,
Cycle,
IntRange,
Submenu,
KeyBind,
Action };
enum class ItemKind : std::uint8_t { TOGGLE,
CYCLE,
INT_RANGE,
SUBMENU,
KEY_BIND,
ACTION };
struct Item {
const char* label;
ItemKind kind;
std::function<std::string()> getValue; // opcional
std::function<void(int dir)> change; // per Toggle/Cycle/IntRange
std::function<void()> enter; // per Submenu i Action
SDL_Scancode* scancode{nullptr}; // per KeyBind
std::function<bool()> visible; // nullptr ⇒ sempre visible
std::function<std::string()> get_value; // opcional
std::function<void(int dir)> change; // per TOGGLE/CYCLE/INT_RANGE
std::function<void()> enter; // per SUBMENU i ACTION
SDL_Scancode* scancode{nullptr}; // per KEY_BIND
std::function<bool()> visible; // nullptr ⇒ sempre visible
};
struct Page {
@@ -78,12 +78,12 @@ namespace Menu {
// Troba el pròxim ítem visible en direcció `dir` (±1) a partir de `from`.
// Si cap és visible retorna `from`.
static auto nextVisibleCursor(const Page& p, int from, int dir) -> int {
const int n = static_cast<int>(p.items.size());
if (n <= 0) {
const int N = static_cast<int>(p.items.size());
if (N <= 0) {
return from;
}
for (int i = 1; i <= n; ++i) {
int idx = ((from + dir * i) % n + n) % n;
for (int i = 1; i <= N; ++i) {
int idx = ((from + dir * i) % N + N) % N;
if (isVisible(p.items[idx])) {
return idx;
}
@@ -92,35 +92,35 @@ namespace Menu {
}
// --- Estat ---
static std::vector<Page> stack_;
static std::unique_ptr<Text> font_;
static float open_anim_{0.0F}; // 0 = tancat, 1 = obert
static float animated_h_{0.0F}; // alçada actual animada (smoothing cap al target visible)
static Uint32 last_ticks_{0};
static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar
static bool closing_{false}; // true mentre l'animació de tancament és en curs
static std::vector<Page> stack;
static std::unique_ptr<Text> font;
static float open_anim{0.0F}; // 0 = tancat, 1 = obert
static float animated_h{0.0F}; // alçada actual animada (smoothing cap al target visible)
static Uint32 last_ticks{0};
static SDL_Scancode* capturing{nullptr}; // != null → esperant tecla per assignar
static bool closing{false}; // true mentre l'animació de tancament és en curs
// --- Transició entre pàgines ---
static constexpr float TRANSITION_SPEED = 5.5F; // ~180 ms
static Page transition_outgoing_{.title = "", .items = {}, .cursor = 0};
static bool transition_active_{false};
static float transition_progress_{1.0F};
static int transition_dir_{+1}; // +1 endavant, -1 enrere
static Page transition_outgoing{.title = "", .items = {}, .cursor = 0};
static bool transition_active{false};
static float transition_progress{1.0F};
static int transition_dir{+1}; // +1 endavant, -1 enrere
// Helpers per triggerar transicions
static void pushPage(Page newPage) {
transition_outgoing_ = stack_.back();
stack_.push_back(std::move(newPage));
transition_active_ = true;
transition_progress_ = 0.0F;
transition_dir_ = +1;
static void pushPage(Page new_page) {
transition_outgoing = stack.back();
stack.push_back(std::move(new_page));
transition_active = true;
transition_progress = 0.0F;
transition_dir = +1;
}
static void popPage() {
transition_outgoing_ = stack_.back();
stack_.pop_back();
transition_active_ = true;
transition_progress_ = 0.0F;
transition_dir_ = -1;
transition_outgoing = stack.back();
stack.pop_back();
transition_active = true;
transition_progress = 0.0F;
transition_dir = -1;
}
// --- Helpers ---
@@ -138,11 +138,11 @@ namespace Menu {
static auto buildRoot() -> Page {
Page p{.title = Locale::get("menu.titles.root"), .items = {}, .cursor = 0};
p.items.push_back({Locale::get("menu.items.video"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildVideo()); }, nullptr});
p.items.push_back({Locale::get("menu.items.audio"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr});
p.items.push_back({Locale::get("menu.items.controls"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr});
p.items.push_back({Locale::get("menu.items.game"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildGame()); }, nullptr});
p.items.push_back({Locale::get("menu.items.system"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildSystem()); }, nullptr});
p.items.push_back({Locale::get("menu.items.video"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildVideo()); }, nullptr});
p.items.push_back({Locale::get("menu.items.audio"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr});
p.items.push_back({Locale::get("menu.items.controls"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr});
p.items.push_back({Locale::get("menu.items.game"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildGame()); }, nullptr});
p.items.push_back({Locale::get("menu.items.system"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildSystem()); }, nullptr});
return p;
}
@@ -151,7 +151,7 @@ namespace Menu {
// Zoom i fullscreen: sense sentit a WASM (el navegador posseix el canvas)
#ifndef __EMSCRIPTEN__
p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::IntRange, [] {
p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::INT_RANGE, [] {
char buf[16];
std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom());
return std::string(buf); }, [](int dir) {
@@ -159,15 +159,15 @@ namespace Menu {
} else if (dir > 0) { Screen::get()->incZoom();
} }, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.screen"), ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.screen"), ItemKind::TOGGLE, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr, nullptr, nullptr});
#endif
// Opcions visuals generals (sempre visibles)
p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::TOGGLE, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::Toggle, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::TOGGLE, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.scaling_mode"), ItemKind::Cycle, [] {
p.items.push_back({Locale::get("menu.items.scaling_mode"), ItemKind::CYCLE, [] {
switch (Options::video.scaling_mode) {
case Options::ScalingMode::DISABLED: return std::string(Locale::get("menu.values.scaling_disabled"));
case Options::ScalingMode::STRETCH: return std::string(Locale::get("menu.values.scaling_stretch"));
@@ -177,30 +177,30 @@ namespace Menu {
}
return std::string(Locale::get("menu.values.scaling_integer")); }, [](int dir) { Screen::get()->cycleScalingMode(dir); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.texture_filter"), ItemKind::Cycle, [] { return std::string(Options::video.texture_filter == Options::TextureFilter::LINEAR
p.items.push_back({Locale::get("menu.items.texture_filter"), ItemKind::CYCLE, [] { return std::string(Options::video.texture_filter == Options::TextureFilter::LINEAR
? Locale::get("menu.values.linear")
: Locale::get("menu.values.nearest")); }, [](int dir) { Screen::get()->cycleTextureFilter(dir); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.internal_resolution"), ItemKind::IntRange, [] {
p.items.push_back({Locale::get("menu.items.internal_resolution"), ItemKind::INT_RANGE, [] {
char buf[16];
std::snprintf(buf, sizeof(buf), "%dX", Options::video.internal_resolution);
return std::string(buf); }, [](int dir) { Screen::get()->changeInternalResolution(dir); }, nullptr, nullptr, nullptr});
// Bloc shaders: no disponible a WASM (NO_SHADERS, sense SDL3 GPU a WebGL2)
#ifndef __EMSCRIPTEN__
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::TOGGLE, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) {
p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::CYCLE, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) {
if (dir < 0) { Screen::get()->prevShaderType();
} else { Screen::get()->nextShaderType();
} }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
p.items.push_back({Locale::get("menu.items.preset"), ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) {
p.items.push_back({Locale::get("menu.items.preset"), ItemKind::CYCLE, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) {
if (dir < 0) { Screen::get()->prevPreset();
} else { Screen::get()->nextPreset();
} }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr, nullptr, [] {
p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::TOGGLE, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr, nullptr, [] {
if (!Options::video.shader_enabled) { return false;
}
const char* name = Screen::get()->getActiveShaderName();
@@ -208,7 +208,7 @@ namespace Menu {
#endif
// Informació de render
p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::Cycle, [] {
p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::CYCLE, [] {
switch (Options::render_info.position) {
case Options::RenderInfoPosition::OFF: return std::string(Locale::get("menu.values.off"));
case Options::RenderInfoPosition::TOP: return std::string(Locale::get("menu.values.top"));
@@ -216,7 +216,7 @@ namespace Menu {
}
return std::string(Locale::get("menu.values.off")); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::Toggle, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr, nullptr, [] { return Options::render_info.position != Options::RenderInfoPosition::OFF; }});
p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::TOGGLE, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr, nullptr, [] { return Options::render_info.position != Options::RenderInfoPosition::OFF; }});
return p;
}
@@ -241,34 +241,34 @@ namespace Menu {
static auto buildControls() -> Page {
Page p{.title = Locale::get("menu.titles.controls"), .items = {}, .cursor = 0};
p.items.push_back({Locale::get("menu.items.move_up"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.up});
p.items.push_back({Locale::get("menu.items.move_down"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.down});
p.items.push_back({Locale::get("menu.items.move_left"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.left});
p.items.push_back({Locale::get("menu.items.move_right"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.right});
p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KeyBind, nullptr, nullptr, nullptr, KeyConfig::scancodePtr("menu_toggle")});
p.items.push_back({Locale::get("menu.items.move_up"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.up});
p.items.push_back({Locale::get("menu.items.move_down"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.down});
p.items.push_back({Locale::get("menu.items.move_left"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.left});
p.items.push_back({Locale::get("menu.items.move_right"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.right});
p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, KeyConfig::scancodePtr("menu_toggle")});
return p;
}
static auto buildAudio() -> Page {
Page p{.title = Locale::get("menu.titles.audio"), .items = {}, .cursor = 0};
p.items.push_back({Locale::get("menu.items.master_enable"), ItemKind::Toggle, [] { return onOff(Options::audio.enabled); }, [](int) {
p.items.push_back({Locale::get("menu.items.master_enable"), ItemKind::TOGGLE, [] { return onOff(Options::audio.enabled); }, [](int) {
Options::audio.enabled = !Options::audio.enabled;
Options::applyAudio(); }, nullptr});
p.items.push_back({Locale::get("menu.items.master_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.volume); }, [](int dir) { stepVolume(Options::audio.volume, dir); }, nullptr});
p.items.push_back({Locale::get("menu.items.master_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.volume); }, [](int dir) { stepVolume(Options::audio.volume, dir); }, nullptr});
p.items.push_back({Locale::get("menu.items.music"), ItemKind::Toggle, [] { return onOff(Options::audio.music.enabled); }, [](int) {
p.items.push_back({Locale::get("menu.items.music"), ItemKind::TOGGLE, [] { return onOff(Options::audio.music.enabled); }, [](int) {
Options::audio.music.enabled = !Options::audio.music.enabled;
Options::applyAudio(); }, nullptr});
p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.music.volume); }, [](int dir) { stepVolume(Options::audio.music.volume, dir); }, nullptr});
p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.music.volume); }, [](int dir) { stepVolume(Options::audio.music.volume, dir); }, nullptr});
p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::Toggle, [] { return onOff(Options::audio.sound.enabled); }, [](int) {
p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::TOGGLE, [] { return onOff(Options::audio.sound.enabled); }, [](int) {
Options::audio.sound.enabled = !Options::audio.sound.enabled;
Options::applyAudio(); }, nullptr});
p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.sound.volume); }, [](int dir) { stepVolume(Options::audio.sound.volume, dir); }, nullptr});
p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.sound.volume); }, [](int dir) { stepVolume(Options::audio.sound.volume, dir); }, nullptr});
return p;
}
@@ -276,11 +276,11 @@ namespace Menu {
static auto buildGame() -> Page {
Page p{.title = Locale::get("menu.titles.game"), .items = {}, .cursor = 0};
p.items.push_back({Locale::get("menu.items.use_new_logo"), ItemKind::Toggle, [] { return yesNo(Options::game.use_new_logo); }, [](int) { Options::game.use_new_logo = !Options::game.use_new_logo; }, nullptr});
p.items.push_back({Locale::get("menu.items.use_new_logo"), ItemKind::TOGGLE, [] { return yesNo(Options::game.use_new_logo); }, [](int) { Options::game.use_new_logo = !Options::game.use_new_logo; }, nullptr});
p.items.push_back({Locale::get("menu.items.show_title_credits"), ItemKind::Toggle, [] { return yesNo(Options::game.show_title_credits); }, [](int) { Options::game.show_title_credits = !Options::game.show_title_credits; }, nullptr});
p.items.push_back({Locale::get("menu.items.show_title_credits"), ItemKind::TOGGLE, [] { return yesNo(Options::game.show_title_credits); }, [](int) { Options::game.show_title_credits = !Options::game.show_title_credits; }, nullptr});
p.items.push_back({Locale::get("menu.items.show_preload"), ItemKind::Toggle, [] { return yesNo(Options::game.show_preload); }, [](int) { Options::game.show_preload = !Options::game.show_preload; }, nullptr});
p.items.push_back({Locale::get("menu.items.show_preload"), ItemKind::TOGGLE, [] { return yesNo(Options::game.show_preload); }, [](int) { Options::game.show_preload = !Options::game.show_preload; }, nullptr});
return p;
}
@@ -289,7 +289,7 @@ namespace Menu {
Page p{.title = Locale::get("menu.titles.system"), .items = {}, .cursor = 0};
p.subtitle = std::string("v") + Texts::VERSION + " (" + Version::GIT_HASH + ")";
p.items.push_back({Locale::get("menu.items.restart"), ItemKind::Action, nullptr, nullptr, [] {
p.items.push_back({Locale::get("menu.items.restart"), ItemKind::ACTION, nullptr, nullptr, [] {
if (Director::get()) {
Director::get()->requestRestart();
}
@@ -298,7 +298,7 @@ namespace Menu {
nullptr});
#ifndef __EMSCRIPTEN__
p.items.push_back({Locale::get("menu.items.exit_game"), ItemKind::Action, nullptr, nullptr, [] {
p.items.push_back({Locale::get("menu.items.exit_game"), ItemKind::ACTION, nullptr, nullptr, [] {
if (Director::get()) {
Director::get()->requestQuit();
}
@@ -314,11 +314,11 @@ namespace Menu {
// Alpha blending per pixel sobre el buffer ARGB (ABGR en memòria)
static void blendRect(Uint32* buf, int x, int y, int w, int h, Uint32 src_argb, Uint8 src_alpha) {
const Uint8 sa = src_alpha;
const Uint8 sr = src_argb & 0xFF;
const Uint8 sg = (src_argb >> 8) & 0xFF;
const Uint8 sb = (src_argb >> 16) & 0xFF;
const Uint8 inv = 255 - sa;
const Uint8 SA = src_alpha;
const Uint8 SR = src_argb & 0xFF;
const Uint8 SG = (src_argb >> 8) & 0xFF;
const Uint8 SB = (src_argb >> 16) & 0xFF;
const Uint8 INV = 255 - SA;
for (int row = y; row < y + h; row++) {
if (row < 0 || row >= SCREEN_H) {
continue;
@@ -332,9 +332,9 @@ namespace Menu {
Uint8 dr = dst & 0xFF;
Uint8 dg = (dst >> 8) & 0xFF;
Uint8 db = (dst >> 16) & 0xFF;
Uint8 r = (sr * sa + dr * inv) / 255;
Uint8 g = (sg * sa + dg * inv) / 255;
Uint8 b = (sb * sa + db * inv) / 255;
Uint8 r = (SR * SA + dr * INV) / 255;
Uint8 g = (SG * SA + dg * INV) / 255;
Uint8 b = (SB * SA + db * INV) / 255;
*p = 0xFF000000U | (static_cast<Uint32>(b) << 16) | (static_cast<Uint32>(g) << 8) | r;
}
}
@@ -365,8 +365,8 @@ namespace Menu {
// body = (N-1) * ITEM_SPACING + charH — així BOTTOM_PAD és el buit real
// sota el text del darrer ítem, no un buit extra per sobre d'un "slot" buit.
static auto boxHeight(const Page& page) -> int {
const int n = static_cast<int>(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); }));
int body = (n == 0) ? 8 : ((n - 1) * ITEM_SPACING) + 8;
const int N = static_cast<int>(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); }));
int body = (N == 0) ? 8 : ((N - 1) * ITEM_SPACING) + 8;
int header = HEADER_H + (page.subtitle.empty() ? 0 : SUBTITLE_H);
return header + body + BOTTOM_PAD;
}
@@ -374,101 +374,104 @@ namespace Menu {
// --- API pública ---
void init() {
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
stack_.clear();
open_anim_ = 0.0F;
closing_ = false;
last_ticks_ = SDL_GetTicks();
font = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
stack.clear();
open_anim = 0.0F;
closing = false;
last_ticks = SDL_GetTicks();
}
void destroy() {
font_.reset();
stack_.clear();
closing_ = false;
font.reset();
stack.clear();
closing = false;
}
// "Actiu": accepta input. Durant l'animació de tancament la pila encara
// té pàgines però ja no ha de processar tecles.
auto isOpen() -> bool {
return !stack_.empty() && !closing_;
return !stack.empty() && !closing;
}
// "Visible": encara hi ha caixa per pintar (incloent close animation).
auto isVisible() -> bool {
return !stack_.empty();
return !stack.empty();
}
void toggle() {
if (closing_ && !stack_.empty()) {
if (closing && !stack.empty()) {
// Cancel·la el tancament en curs — continua l'animació cap a "obert"
// des del valor actual d'open_anim_.
closing_ = false;
last_ticks_ = SDL_GetTicks();
// des del valor actual d'open_anim.
closing = false;
last_ticks = SDL_GetTicks();
return;
}
if (isOpen()) {
close();
} else {
stack_.push_back(buildRoot());
open_anim_ = 0.0F;
closing_ = false;
animated_h_ = static_cast<float>(boxHeight(stack_.back()));
last_ticks_ = SDL_GetTicks();
stack.push_back(buildRoot());
open_anim = 0.0F;
closing = false;
animated_h = static_cast<float>(boxHeight(stack.back()));
last_ticks = SDL_GetTicks();
}
}
// close() no buida la pila immediatament: marca closing_ i deixa que
// render() faça decréixer open_anim_ fins a 0. En aquell moment es neteja
// close() no buida la pila immediatament: marca closing i deixa que
// render() faça decréixer open_anim fins a 0. En aquell moment es neteja
// l'estat. Si es crida estant ja tancat o tancant-se, no-op.
void close() {
if (stack_.empty() || closing_) {
if (stack.empty() || closing) {
return;
}
closing_ = true;
capturing_ = nullptr;
transition_active_ = false;
transition_progress_ = 1.0F;
last_ticks_ = SDL_GetTicks();
closing = true;
capturing = nullptr;
transition_active = false;
transition_progress = 1.0F;
last_ticks = SDL_GetTicks();
}
auto isCapturing() -> bool {
return capturing_ != nullptr;
return capturing != nullptr;
}
void captureKey(SDL_Scancode sc) {
if (capturing_ == nullptr) {
if (capturing == nullptr) {
return;
}
if (sc == SDL_SCANCODE_ESCAPE) {
// Cancel·la
capturing_ = nullptr;
capturing = nullptr;
return;
}
*capturing_ = sc;
capturing_ = nullptr;
*capturing = sc;
capturing = nullptr;
}
void handleKey(SDL_Scancode sc) {
if (!isOpen()) {
return;
static void backOrClose() {
if (stack.size() > 1) {
popPage();
} else {
close();
}
Page& page = stack_.back();
if (page.items.empty()) {
// Pàgina buida — només backspace surt
if (sc == SDL_SCANCODE_BACKSPACE) {
if (stack_.size() > 1) {
popPage();
} else {
close();
}
}
// Activació d'un ítem (RETURN/KP_ENTER): SUBMENU/ACTION criden enter,
// KEY_BIND inicia captura, la resta avança change(+1).
static void activateItem(Item& item) {
if (item.kind == ItemKind::SUBMENU || item.kind == ItemKind::ACTION) {
if (item.enter) {
item.enter();
}
return;
}
// Si el cursor està sobre un ítem ocultat (p. ex. una acció anterior el va ocultar),
// reubica'l al pròxim visible abans de processar l'entrada.
if (!isVisible(page.items[page.cursor])) {
page.cursor = nextVisibleCursor(page, page.cursor, +1);
} else if (item.kind == ItemKind::KEY_BIND) {
capturing = item.scancode;
} else if (item.change) {
item.change(+1);
}
}
static void applyKeyToItem(Page& page, SDL_Scancode sc) {
Item& item = page.items[page.cursor];
switch (sc) {
case SDL_SCANCODE_UP:
page.cursor = nextVisibleCursor(page, page.cursor, -1);
@@ -477,62 +480,66 @@ namespace Menu {
page.cursor = nextVisibleCursor(page, page.cursor, +1);
break;
case SDL_SCANCODE_LEFT:
if (page.items[page.cursor].kind != ItemKind::Submenu &&
page.items[page.cursor].change) {
page.items[page.cursor].change(-1);
if (item.kind != ItemKind::SUBMENU && item.change) {
item.change(-1);
}
break;
case SDL_SCANCODE_RIGHT:
if (page.items[page.cursor].kind != ItemKind::Submenu &&
page.items[page.cursor].change) {
page.items[page.cursor].change(+1);
if (item.kind != ItemKind::SUBMENU && item.change) {
item.change(+1);
}
break;
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
if (page.items[page.cursor].kind == ItemKind::Submenu ||
page.items[page.cursor].kind == ItemKind::Action) {
if (page.items[page.cursor].enter) {
page.items[page.cursor].enter();
}
} else if (page.items[page.cursor].kind == ItemKind::KeyBind) {
capturing_ = page.items[page.cursor].scancode;
} else if (page.items[page.cursor].change) {
page.items[page.cursor].change(+1);
}
activateItem(item);
break;
case SDL_SCANCODE_BACKSPACE:
if (stack_.size() > 1) {
popPage();
} else {
close();
}
backOrClose();
break;
default:
break;
}
// Després de qualsevol acció, si el cursor quedara sobre un ítem ocult
// (possible si una acció ha canviat la visibilitat pròpia de l'ítem actual,
// edge case defensiu), salta al següent visible.
if (!stack_.empty()) {
Page& top = stack_.back();
}
void handleKey(SDL_Scancode sc) {
if (!isOpen()) {
return;
}
Page& page = stack.back();
if (page.items.empty()) {
if (sc == SDL_SCANCODE_BACKSPACE) {
backOrClose();
}
return;
}
if (!isVisible(page.items[page.cursor])) {
page.cursor = nextVisibleCursor(page, page.cursor, +1);
}
applyKeyToItem(page, sc);
// Defensa: si una acció ha amagat l'ítem que tenim sota el cursor,
// saltem al pròxim visible.
if (!stack.empty()) {
Page& top = stack.back();
if (!top.items.empty() && !isVisible(top.items[top.cursor])) {
top.cursor = nextVisibleCursor(top, top.cursor, +1);
}
}
}
// Forward decl: renderOneItem viu sota renderPageContent però aquest l'ha de cridar.
static void renderOneItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max);
// Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip.
// box_x/box_y són les coordenades de la caixa (per calcular posicions relatives);
// clip_x_min/clip_x_max limiten on es dibuixa text i la línia separadora.
static void renderPageContent(Uint32* pixel_data, const Page& page, int box_x, int box_y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
// Títol
int title_w = font_->width(page.title);
int title_w = font->width(page.title);
int title_x = box_x + ((BOX_W - title_w) / 2) + x_offset;
font_->drawClipped(pixel_data, title_x, box_y + TITLE_PAD_Y, page.title, TITLE_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
font->drawClipped(pixel_data, title_x, box_y + TITLE_PAD_Y, page.title, TITLE_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
// Línia sota el títol (també lliscada) — clippada manualment
int title_line_y = box_y + TITLE_PAD_Y + font_->charHeight() + 2;
int title_line_y = box_y + TITLE_PAD_Y + font->charHeight() + 2;
if (title_line_y >= clip_y_min && title_line_y < clip_y_max) {
int line_x = box_x + 4 + x_offset;
int line_w = BOX_W - 8;
@@ -546,143 +553,162 @@ namespace Menu {
// Subtítol opcional (sota la línia del títol, abans dels items)
int items_y = title_line_y + 4;
if (!page.subtitle.empty()) {
int sub_w = font_->width(page.subtitle.c_str());
int sub_w = font->width(page.subtitle.c_str());
int sub_x = box_x + ((BOX_W - sub_w) / 2) + x_offset;
font_->drawClipped(pixel_data, sub_x, items_y, page.subtitle.c_str(), LABEL_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
font->drawClipped(pixel_data, sub_x, items_y, page.subtitle.c_str(), LABEL_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
items_y += SUBTITLE_H;
}
// Compta visibles — si cap, dibuixa placeholder (caixa totalment col·lapsada però oberta)
const int visible_count = static_cast<int>(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); }));
if (visible_count == 0) {
const int VISIBLE_COUNT = static_cast<int>(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); }));
if (VISIBLE_COUNT == 0) {
const char* empty_text = Locale::get("menu.values.empty");
int ew = font_->width(empty_text);
font_->drawClipped(pixel_data, box_x + ((BOX_W - ew) / 2) + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
int ew = font->width(empty_text);
font->drawClipped(pixel_data, box_x + ((BOX_W - ew) / 2) + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
return;
}
int y_slot = 0; // índex de fila visible (independent de l'índex real de l'ítem)
int y_slot = 0;
for (size_t i = 0; i < page.items.size(); i++) {
const Item& item = page.items[i];
if (!isVisible(item)) {
continue;
}
int y = items_y + (y_slot * ITEM_SPACING);
const int Y = items_y + (y_slot * ITEM_SPACING);
++y_slot;
bool selected = (static_cast<int>(i) == page.cursor);
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
// Action: sense valor a la dreta — centrem el label amb el cursor just a l'esquerra.
if (item.kind == ItemKind::Action) {
int lw = font_->width(item.label);
int lx = box_x + ((BOX_W - lw) / 2) + x_offset;
if (selected) {
font_->drawClipped(pixel_data, lx - font_->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
font_->drawClipped(pixel_data, lx, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
continue;
}
if (selected) {
font_->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
font_->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
if (item.kind == ItemKind::Submenu) {
const char* arrow = ">>";
int aw = font_->width(arrow);
Uint32 ac = selected ? CURSOR_COLOR : VALUE_COLOR;
font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - aw + x_offset, y, arrow, ac, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
} else if (item.kind == ItemKind::KeyBind) {
bool this_capturing = (capturing_ == item.scancode);
const char* text = nullptr;
if (this_capturing) {
text = Locale::get("menu.values.press_key");
} else if (item.scancode != nullptr) {
text = SDL_GetScancodeName(*item.scancode);
} else {
text = "";
}
if ((text == nullptr) || (*text == 0)) {
text = Locale::get("menu.values.unknown");
}
int tw = font_->width(text);
Uint32 tc = 0;
if (this_capturing) {
tc = 0xFF00FFFF;
} else {
tc = selected ? CURSOR_COLOR : VALUE_COLOR;
}
font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - tw + x_offset, y, text, tc, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
} else if (item.getValue) {
std::string value = item.getValue();
int value_w = font_->width(value.c_str());
Uint32 value_color = selected ? CURSOR_COLOR : VALUE_COLOR;
font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - value_w + x_offset, y, value.c_str(), value_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
const bool SELECTED = (static_cast<int>(i) == page.cursor);
renderOneItem(pixel_data, item, SELECTED, box_x, Y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
}
static auto keybindText(const Item& item, bool this_capturing) -> const char* {
const char* text = nullptr;
if (this_capturing) {
text = Locale::get("menu.values.press_key");
} else if (item.scancode != nullptr) {
text = SDL_GetScancodeName(*item.scancode);
} else {
text = "";
}
if ((text == nullptr) || (*text == 0)) {
text = Locale::get("menu.values.unknown");
}
return text;
}
static void renderActionItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
const Uint32 LABEL_COLOR_LOCAL = selected ? CURSOR_COLOR : LABEL_COLOR;
const int LW = font->width(item.label);
const int LX = box_x + ((BOX_W - LW) / 2) + x_offset;
if (selected) {
font->drawClipped(pixel_data, LX - font->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
font->drawClipped(pixel_data, LX, y, item.label, LABEL_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
static auto keybindColor(bool this_capturing, bool selected) -> Uint32 {
if (this_capturing) {
return 0xFF00FFFF;
}
return selected ? CURSOR_COLOR : VALUE_COLOR;
}
static void renderItemValue(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
if (item.kind == ItemKind::SUBMENU) {
const char* arrow = ">>";
const int AW = font->width(arrow);
const Uint32 AC = selected ? CURSOR_COLOR : VALUE_COLOR;
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - AW + x_offset, y, arrow, AC, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
return;
}
if (item.kind == ItemKind::KEY_BIND) {
const bool THIS_CAPTURING = (capturing == item.scancode);
const char* text = keybindText(item, THIS_CAPTURING);
const int TW = font->width(text);
const Uint32 TC = keybindColor(THIS_CAPTURING, selected);
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - TW + x_offset, y, text, TC, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
return;
}
if (item.get_value) {
const std::string VALUE = item.get_value();
const int VALUE_W = font->width(VALUE.c_str());
const Uint32 VALUE_COLOR_LOCAL = selected ? CURSOR_COLOR : VALUE_COLOR;
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - VALUE_W + x_offset, y, VALUE.c_str(), VALUE_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
}
static void renderOneItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
if (item.kind == ItemKind::ACTION) {
renderActionItem(pixel_data, item, selected, box_x, y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
return;
}
const Uint32 LABEL_COLOR_LOCAL = selected ? CURSOR_COLOR : LABEL_COLOR;
if (selected) {
font->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
font->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, LABEL_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
renderItemValue(pixel_data, item, selected, box_x, y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
void render(Uint32* pixel_data) {
if (!isVisible() || !font_ || (pixel_data == nullptr)) {
if (!isVisible() || !font || (pixel_data == nullptr)) {
return;
}
// Delta time
Uint32 now = SDL_GetTicks();
float dt = static_cast<float>(now - last_ticks_) / 1000.0F;
last_ticks_ = now;
if (closing_) {
open_anim_ -= CLOSE_SPEED * dt;
if (open_anim_ <= 0.0F) {
float dt = static_cast<float>(now - last_ticks) / 1000.0F;
last_ticks = now;
if (closing) {
open_anim -= CLOSE_SPEED * dt;
if (open_anim <= 0.0F) {
// Animació de tancament completada — buida l'estat de veritat.
open_anim_ = 0.0F;
stack_.clear();
animated_h_ = 0.0F;
closing_ = false;
open_anim = 0.0F;
stack.clear();
animated_h = 0.0F;
closing = false;
return;
}
} else if (open_anim_ < 1.0F) {
open_anim_ += OPEN_SPEED * dt;
open_anim_ = std::min(open_anim_, 1.0F);
} else if (open_anim < 1.0F) {
open_anim += OPEN_SPEED * dt;
open_anim = std::min(open_anim, 1.0F);
}
// Avança transició
if (transition_active_) {
transition_progress_ += TRANSITION_SPEED * dt;
if (transition_progress_ >= 1.0F) {
transition_progress_ = 1.0F;
transition_active_ = false;
if (transition_active) {
transition_progress += TRANSITION_SPEED * dt;
if (transition_progress >= 1.0F) {
transition_progress = 1.0F;
transition_active = false;
}
}
const Page& page = stack_.back();
const int current_h = boxHeight(page);
const Page& page = stack.back();
const int CURRENT_H = boxHeight(page);
// Smoothing exponencial de l'alçada cap al target (pàgina actual + ítems visibles).
// Permet que el menú reaccione amb animació quan una opció canvia la visibilitat
// d'altres ítems en calent (p. ex. shader=off → shader_type/preset/supersampling).
if (animated_h_ <= 0.0F) {
animated_h_ = static_cast<float>(current_h);
if (animated_h <= 0.0F) {
animated_h = static_cast<float>(CURRENT_H);
} else {
float diff = static_cast<float>(current_h) - animated_h_;
float diff = static_cast<float>(CURRENT_H) - animated_h;
if (std::fabs(diff) < 0.5F) {
animated_h_ = static_cast<float>(current_h);
animated_h = static_cast<float>(CURRENT_H);
} else {
float t = HEIGHT_RATE * dt;
t = std::min(t, 1.0F);
animated_h_ += diff * t;
animated_h += diff * t;
}
}
float eased = Easing::outQuad(open_anim_);
float eased = Easing::outQuad(open_anim);
// Calcula alçada (amb transició si escau)
int target_h = static_cast<int>(animated_h_);
if (transition_active_) {
int outgoing_h = boxHeight(transition_outgoing_);
float tp = Easing::outQuad(transition_progress_);
target_h = Easing::lerpInt(outgoing_h, static_cast<int>(animated_h_), tp);
int target_h = static_cast<int>(animated_h);
if (transition_active) {
int outgoing_h = boxHeight(transition_outgoing);
float tp = Easing::outQuad(transition_progress);
target_h = Easing::lerpInt(outgoing_h, static_cast<int>(animated_h), tp);
}
// Caixa creix verticalment durant l'obertura
@@ -696,17 +722,17 @@ namespace Menu {
blendRect(pixel_data, box_x, box_y, BOX_W, box_h, BG_COLOR, alpha);
// El contingut només apareix quan la caixa és prou gran
if (open_anim_ >= 0.9F) {
if (open_anim >= 0.9F) {
int clip_x_min = box_x + 1;
int clip_x_max = box_x + BOX_W - 1;
int clip_y_min = box_y + 1;
int clip_y_max = box_y + box_h - 1;
if (transition_active_) {
float tp = Easing::outQuad(transition_progress_);
int out_offset = static_cast<int>(-transition_dir_ * BOX_W * tp);
int new_offset = static_cast<int>(transition_dir_ * BOX_W * (1.0F - tp));
renderPageContent(pixel_data, transition_outgoing_, box_x, box_y, out_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
if (transition_active) {
float tp = Easing::outQuad(transition_progress);
int out_offset = static_cast<int>(-transition_dir * BOX_W * tp);
int new_offset = static_cast<int>(transition_dir * BOX_W * (1.0F - tp));
renderPageContent(pixel_data, transition_outgoing, box_x, box_y, out_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
renderPageContent(pixel_data, page, box_x, box_y, new_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
} else {
renderPageContent(pixel_data, page, box_x, box_y, 0, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
+252 -269
View File
@@ -14,7 +14,7 @@
namespace Overlay {
static std::unique_ptr<Text> font_;
static std::unique_ptr<Text> font;
// --- Aspecte de la notificació ---
static constexpr Uint32 NOTIF_BG_COLOR = 0xFF2E1A1A; // Fons blau fosc (ABGR)
@@ -52,12 +52,12 @@ namespace Overlay {
int box_h{0}; // Alçada de la caixa (calculat al crear)
};
static std::vector<Notification> notifications_;
static Uint32 last_ticks_ = 0;
static std::vector<Notification> notifications;
static Uint32 last_ticks = 0;
// --- Render info ---
static Options::RenderInfoPosition info_visible_pos_ = Options::RenderInfoPosition::OFF;
static float info_anim_ = 0.0F; // 0 = fora de pantalla, 1 = posició final
static Options::RenderInfoPosition info_visible_pos = Options::RenderInfoPosition::OFF;
static float info_anim = 0.0F; // 0 = fora de pantalla, 1 = posició final
static constexpr float INFO_SLIDE_SPEED = 5.0F;
// Segments del render info — cadascú amb la seva pròpia visibilitat animada
@@ -69,7 +69,7 @@ namespace Overlay {
bool visible{false};
bool mono_digits{false}; // si true, dígits amb amplada fixa (la resta natural)
};
static InfoSegment info_segments_[INFO_SEGMENT_COUNT];
static InfoSegment info_segments[INFO_SEGMENT_COUNT];
// --- Crèdits cinematogràfics ---
// Usen el sistema de notificacions en posició TOP_CENTER_DROP.
@@ -78,8 +78,8 @@ namespace Overlay {
PLAYING_1,
GAP,
PLAYING_2 };
static CreditsPhase credits_phase_ = CreditsPhase::IDLE;
static float credits_timer_ = 0.0F; // segons dins la phase actual
static CreditsPhase credits_phase = CreditsPhase::IDLE;
static float credits_timer = 0.0F; // segons dins la phase actual
static constexpr float CREDITS_DELAY = 2.0F;
static constexpr float CREDITS_GAP = 0.4F;
static constexpr float CREDITS_HOLD = 7.5F;
@@ -87,16 +87,16 @@ namespace Overlay {
static constexpr Uint32 CREDITS_FG = NOTIF_TEXT_COLOR; // mateix cian
// --- Doble ESC per a eixir ---
static bool esc_waiting_ = false;
static bool esc_waiting = false;
void init() {
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
last_ticks_ = SDL_GetTicks();
font = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
last_ticks = SDL_GetTicks();
}
void destroy() {
font_.reset();
notifications_.clear();
font.reset();
notifications.clear();
}
// Pinta un rectangle sòlid dins els límits de la pantalla
@@ -114,258 +114,241 @@ namespace Overlay {
}
}
void render(Uint32* pixel_data) {
if (!font_ || (pixel_data == nullptr)) {
static void updateNotifFsm(Notification& notif, float dt) {
switch (notif.status) {
case Status::RISING:
notif.anim += SLIDE_SPEED * dt;
if (notif.anim >= 1.0F) {
notif.anim = 1.0F;
notif.status = Status::STAY;
notif.timer = 0.0F;
}
break;
case Status::STAY:
notif.timer += dt;
if (notif.timer >= notif.duration) {
notif.status = Status::VANISHING;
}
break;
case Status::VANISHING:
notif.anim -= SLIDE_SPEED * dt;
if (notif.anim <= 0.0F) {
notif.status = Status::FINISHED;
}
break;
case Status::FINISHED:
break;
}
}
static void computeNotifBoxPos(const Notification& notif, int& box_x, int& box_y) {
switch (notif.pos) {
case NotifPosition::TOP_LEFT_SLIDE:
box_x = NOTIF_MARGIN_X - static_cast<int>((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X));
box_y = NOTIF_MARGIN_Y;
break;
case NotifPosition::TOP_CENTER_DROP:
box_x = (SCREEN_W - notif.box_w) / 2;
box_y = NOTIF_MARGIN_Y - static_cast<int>((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y));
break;
}
}
static void drawNotifTextLine(Uint32* pixel_data, const std::string& line, int line_x, int line_y, const Notification& notif) {
if (notif.style == NotifStyle::SHADOW) {
font->draw(pixel_data, line_x + 1, line_y + 1, line.c_str(), notif.accent_color);
} else if (notif.style == NotifStyle::OUTLINE) {
font->draw(pixel_data, line_x, line_y - 1, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x, line_y + 1, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x - 1, line_y, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x + 1, line_y, line.c_str(), notif.accent_color);
}
font->draw(pixel_data, line_x, line_y, line.c_str(), notif.text_color);
}
static void renderOneNotification(Uint32* pixel_data, const Notification& notif) {
int box_x = 0;
int box_y = 0;
computeNotifBoxPos(notif, box_x, box_y);
if (notif.style == NotifStyle::BOX) {
drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color);
}
const int LINE_H = font->charHeight();
int line_y = box_y + NOTIF_PADDING_V;
for (const auto& line : notif.lines) {
const int LINE_W = font->width(line.c_str());
const int LINE_X = box_x + ((notif.box_w - LINE_W) / 2);
drawNotifTextLine(pixel_data, line, LINE_X, line_y, notif);
line_y += LINE_H + 1;
}
}
static void updateRenderInfoFsm(float dt) {
const auto DESIRED = Options::render_info.position;
if (DESIRED == info_visible_pos) {
if (info_anim < 1.0F) {
info_anim = std::min(info_anim + (INFO_SLIDE_SPEED * dt), 1.0F);
}
} else if (info_visible_pos == Options::RenderInfoPosition::OFF) {
info_visible_pos = DESIRED;
info_anim = 0.0F;
} else {
info_anim -= INFO_SLIDE_SPEED * dt;
if (info_anim <= 0.0F) {
info_anim = 0.0F;
info_visible_pos = DESIRED;
}
}
for (auto& seg : info_segments) {
const float TARGET = seg.visible ? 1.0F : 0.0F;
if (seg.anim < TARGET) {
seg.anim = std::min(seg.anim + (SEG_SPEED * dt), TARGET);
} else if (seg.anim > TARGET) {
seg.anim = std::max(seg.anim - (SEG_SPEED * dt), TARGET);
}
}
}
static auto computeInfoTotalWidth(int digit_cell) -> float {
float total_w = 0.0F;
for (const auto& seg : info_segments) {
if (seg.anim > 0.0F && !seg.text.empty()) {
const int W = seg.mono_digits
? font->widthMonoDigits(seg.text.c_str(), digit_cell)
: font->width(seg.text.c_str());
total_w += static_cast<float>(W) * Easing::outQuad(seg.anim);
}
}
return total_w;
}
static void drawInfoSegment(Uint32* pixel_data, const InfoSegment& seg, int xi, int info_y, int digit_cell) {
if (seg.mono_digits) {
font->drawMonoDigits(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color, digit_cell);
font->drawMonoDigits(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color, digit_cell);
} else {
font->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color);
font->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color);
}
}
static void renderRenderInfo(Uint32* pixel_data) {
if (info_visible_pos == Options::RenderInfoPosition::OFF || info_anim <= 0.0F) {
return;
}
// Calcula delta time
Uint32 now = SDL_GetTicks();
float dt = static_cast<float>(now - last_ticks_) / 1000.0F;
last_ticks_ = now;
// Actualitza i pinta cada notificació
for (auto& notif : notifications_) {
switch (notif.status) {
case Status::RISING:
notif.anim += SLIDE_SPEED * dt;
if (notif.anim >= 1.0F) {
notif.anim = 1.0F;
notif.status = Status::STAY;
notif.timer = 0.0F;
}
break;
case Status::STAY:
notif.timer += dt;
if (notif.timer >= notif.duration) {
notif.status = Status::VANISHING;
}
break;
case Status::VANISHING:
notif.anim -= SLIDE_SPEED * dt;
if (notif.anim <= 0.0F) {
notif.status = Status::FINISHED;
}
break;
case Status::FINISHED:
break;
}
if (notif.status == Status::FINISHED) {
const int DIGIT_CELL = font->charBoxWidth() - 1;
const float TOTAL_W = computeInfoTotalWidth(DIGIT_CELL);
if (TOTAL_W <= 0.0F) {
return;
}
const float EASED_Y = Easing::outQuad(info_anim);
const int CH = font->charHeight();
const int FINAL_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? 1 : SCREEN_H - CH - 1;
const int START_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? -CH - 1 : SCREEN_H;
const int INFO_Y = START_Y + static_cast<int>(static_cast<float>(FINAL_Y - START_Y) * EASED_Y);
float cur_x = (SCREEN_W - TOTAL_W) / 2.0F;
for (const auto& seg : info_segments) {
if (seg.anim <= 0.01F || seg.text.empty()) {
continue;
}
const int XI = static_cast<int>(cur_x);
const int SEG_W = seg.mono_digits
? font->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
: font->width(seg.text.c_str());
drawInfoSegment(pixel_data, seg, XI, INFO_Y, DIGIT_CELL);
cur_x += static_cast<float>(SEG_W) * Easing::outQuad(seg.anim);
}
}
// Posició segons el tipus
int box_x = 0;
int box_y = 0;
switch (notif.pos) {
case NotifPosition::TOP_LEFT_SLIDE: {
int target_x = NOTIF_MARGIN_X;
int target_y = NOTIF_MARGIN_Y;
box_x = target_x - static_cast<int>((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X));
box_y = target_y;
break;
}
case NotifPosition::TOP_CENTER_DROP: {
int target_y = NOTIF_MARGIN_Y;
box_x = (SCREEN_W - notif.box_w) / 2;
// Baixa des de sobre de la pantalla fins a target_y
box_y = target_y - static_cast<int>((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y));
break;
}
}
static void renderPauseIndicator(Uint32* pixel_data) {
if ((Director::get() == nullptr) || !Director::get()->isPaused()) {
return;
}
const char* pause_text = Locale::get("notifications.pause");
const int W = font->width(pause_text);
const int X = SCREEN_W - W - 4;
const int Y = 4;
font->draw(pixel_data, X, Y - 1, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X, Y + 1, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X - 1, Y, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X + 1, Y, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X, Y, pause_text, 0xFF0000FF);
}
// Pinta fons (si BOX)
if (notif.style == NotifStyle::BOX) {
drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color);
}
static void emitCreditsLines(const char* role_key, const char* name_key) {
showNotification(
{std::string(Locale::get(role_key)), std::string(Locale::get(name_key))},
CREDITS_HOLD,
NotifPosition::TOP_CENTER_DROP,
NotifStyle::OUTLINE,
CREDITS_BG,
CREDITS_FG);
}
// Pinta el text línia a línia (amb ombra o contorn segons style)
int line_h = font_->charHeight();
int line_y = box_y + NOTIF_PADDING_V;
for (const auto& line : notif.lines) {
int line_w = font_->width(line.c_str());
int line_x = box_x + ((notif.box_w - line_w) / 2); // centrat dins la caixa
if (notif.style == NotifStyle::SHADOW) {
font_->draw(pixel_data, line_x + 1, line_y + 1, line.c_str(), notif.accent_color);
} else if (notif.style == NotifStyle::OUTLINE) {
// Contorn 4-direccional (N, S, E, W)
font_->draw(pixel_data, line_x, line_y - 1, line.c_str(), notif.accent_color);
font_->draw(pixel_data, line_x, line_y + 1, line.c_str(), notif.accent_color);
font_->draw(pixel_data, line_x - 1, line_y, line.c_str(), notif.accent_color);
font_->draw(pixel_data, line_x + 1, line_y, line.c_str(), notif.accent_color);
static void advanceCredits(float dt) {
if (credits_phase == CreditsPhase::IDLE) {
return;
}
credits_timer += dt;
switch (credits_phase) {
case CreditsPhase::DELAY:
if (credits_timer >= CREDITS_DELAY) {
emitCreditsLines("credits.port_role", "credits.port_name");
credits_phase = CreditsPhase::PLAYING_1;
credits_timer = 0.0F;
}
font_->draw(pixel_data, line_x, line_y, line.c_str(), notif.text_color);
line_y += line_h + 1;
break;
case CreditsPhase::PLAYING_1:
if (notifications.empty()) {
credits_phase = CreditsPhase::GAP;
credits_timer = 0.0F;
}
break;
case CreditsPhase::GAP:
if (credits_timer >= CREDITS_GAP) {
emitCreditsLines("credits.modern_role", "credits.modern_name");
credits_phase = CreditsPhase::PLAYING_2;
credits_timer = 0.0F;
}
break;
case CreditsPhase::PLAYING_2:
if (notifications.empty()) {
credits_phase = CreditsPhase::IDLE;
credits_timer = 0.0F;
}
break;
case CreditsPhase::IDLE:
break;
}
}
void render(Uint32* pixel_data) {
if (!font || (pixel_data == nullptr)) {
return;
}
const Uint32 NOW = SDL_GetTicks();
const float DT = static_cast<float>(NOW - last_ticks) / 1000.0F;
last_ticks = NOW;
for (auto& notif : notifications) {
updateNotifFsm(notif, DT);
if (notif.status != Status::FINISHED) {
renderOneNotification(pixel_data, notif);
}
}
// Render info (FPS, driver, shader) — animat amb slide vertical
// State machine: visible_pos s'actualitza cap a desired quan anim arriba a 0
{
const auto desired = Options::render_info.position;
if (desired == info_visible_pos_) {
// Mateix lloc: entra fins a 1
if (info_anim_ < 1.0F) {
info_anim_ += INFO_SLIDE_SPEED * dt;
info_anim_ = std::min(info_anim_, 1.0F);
}
} else {
// Canvi: si visible_pos està OFF, commuta directament
if (info_visible_pos_ == Options::RenderInfoPosition::OFF) {
info_visible_pos_ = desired;
info_anim_ = 0.0F;
} else {
// Ix del lloc actual
info_anim_ -= INFO_SLIDE_SPEED * dt;
if (info_anim_ <= 0.0F) {
info_anim_ = 0.0F;
info_visible_pos_ = desired;
}
}
}
updateRenderInfoFsm(DT);
renderRenderInfo(pixel_data);
// Actualitza animacions individuals dels segments
for (auto& seg : info_segments_) {
float target = seg.visible ? 1.0F : 0.0F;
if (seg.anim < target) {
seg.anim += SEG_SPEED * dt;
seg.anim = std::min(seg.anim, target);
} else if (seg.anim > target) {
seg.anim -= SEG_SPEED * dt;
seg.anim = std::max(seg.anim, target);
}
}
// Render si hi ha alguna cosa visible
if (info_visible_pos_ != Options::RenderInfoPosition::OFF && info_anim_ > 0.0F) {
const int DIGIT_CELL = font_->charBoxWidth() - 1; // amplada uniforme per dígit
// Calcula amplada total interpolant cada segment per la seva anim
float total_w = 0.0F;
for (const auto& seg : info_segments_) {
if (seg.anim > 0.0F && !seg.text.empty()) {
int w = seg.mono_digits
? font_->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
: font_->width(seg.text.c_str());
total_w += w * Easing::outQuad(seg.anim);
}
}
if (total_w > 0.0F) {
float eased_y = Easing::outQuad(info_anim_);
int ch = font_->charHeight();
int final_y;
int start_y;
if (info_visible_pos_ == Options::RenderInfoPosition::TOP) {
final_y = 1;
start_y = -ch - 1;
} else {
final_y = SCREEN_H - ch - 1;
start_y = SCREEN_H;
}
int info_y = start_y + static_cast<int>((final_y - start_y) * eased_y);
// Dibuixa cada segment en la seva posició x acumulada
float cur_x = (SCREEN_W - total_w) / 2.0F;
for (const auto& seg : info_segments_) {
if (seg.anim > 0.01F && !seg.text.empty()) {
int xi = static_cast<int>(cur_x);
int seg_w = seg.mono_digits
? font_->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
: font_->width(seg.text.c_str());
if (seg.mono_digits) {
font_->drawMonoDigits(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color, DIGIT_CELL);
font_->drawMonoDigits(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color, DIGIT_CELL);
} else {
font_->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color);
font_->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color);
}
cur_x += seg_w * Easing::outQuad(seg.anim);
}
}
}
}
std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; });
if (esc_waiting && notifications.empty()) {
esc_waiting = false;
}
// Elimina les acabades
std::erase_if(notifications_, [](const Notification& n) { return n.status == Status::FINISHED; });
renderPauseIndicator(pixel_data);
advanceCredits(DT);
// Si la notificació d'ESC ha desaparegut, reseteja l'estat
if (esc_waiting_ && notifications_.empty()) {
esc_waiting_ = false;
}
// Indicador de pausa persistent (cantó superior dret)
if ((Director::get() != nullptr) && Director::get()->isPaused()) {
const char* pause_text = Locale::get("notifications.pause");
int w = font_->width(pause_text);
int x = SCREEN_W - w - 4;
int y = 4;
// Contorn blanc 4-direccional
font_->draw(pixel_data, x, y - 1, pause_text, 0xFFFFFFFF);
font_->draw(pixel_data, x, y + 1, pause_text, 0xFFFFFFFF);
font_->draw(pixel_data, x - 1, y, pause_text, 0xFFFFFFFF);
font_->draw(pixel_data, x + 1, y, pause_text, 0xFFFFFFFF);
// Text en roig
font_->draw(pixel_data, x, y, pause_text, 0xFF0000FF);
}
// Crèdits seqüencials — dispara notificacions TOP_CENTER_DROP una darrere l'altra.
if (credits_phase_ != CreditsPhase::IDLE) {
credits_timer_ += dt;
switch (credits_phase_) {
case CreditsPhase::DELAY:
if (credits_timer_ >= CREDITS_DELAY) {
showNotification(
{std::string(Locale::get("credits.port_role")),
std::string(Locale::get("credits.port_name"))},
CREDITS_HOLD,
NotifPosition::TOP_CENTER_DROP,
NotifStyle::OUTLINE,
CREDITS_BG,
CREDITS_FG);
credits_phase_ = CreditsPhase::PLAYING_1;
credits_timer_ = 0.0F;
}
break;
case CreditsPhase::PLAYING_1:
if (notifications_.empty()) {
credits_phase_ = CreditsPhase::GAP;
credits_timer_ = 0.0F;
}
break;
case CreditsPhase::GAP:
if (credits_timer_ >= CREDITS_GAP) {
showNotification(
{std::string(Locale::get("credits.modern_role")),
std::string(Locale::get("credits.modern_name"))},
CREDITS_HOLD,
NotifPosition::TOP_CENTER_DROP,
NotifStyle::OUTLINE,
CREDITS_BG,
CREDITS_FG);
credits_phase_ = CreditsPhase::PLAYING_2;
credits_timer_ = 0.0F;
}
break;
case CreditsPhase::PLAYING_2:
if (notifications_.empty()) {
credits_phase_ = CreditsPhase::IDLE;
credits_timer_ = 0.0F;
}
break;
case CreditsPhase::IDLE:
break;
}
}
// Neteja notificacions finalitzades
std::erase_if(notifications_, [](const Notification& n) { return n.status == Status::FINISHED; });
// Menú flotant per damunt de tot (isVisible inclou l'animació de tancament)
std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; });
if (Menu::isVisible()) {
Menu::render(pixel_data);
}
@@ -382,7 +365,7 @@ namespace Overlay {
Uint32 accent_color,
Uint32 text_color) {
// Reemplaça la notificació anterior
notifications_.clear();
notifications.clear();
Notification notif;
notif.lines = lines;
@@ -395,15 +378,15 @@ namespace Overlay {
// Calcula l'amplada màxima de les línies
int max_w = 0;
for (const auto& line : lines) {
int w = font_->width(line.c_str());
int w = font->width(line.c_str());
max_w = std::max(w, max_w);
}
notif.box_w = max_w + NOTIF_PADDING_H * 2;
int line_h = font_->charHeight();
int line_h = font->charHeight();
int line_count = static_cast<int>(lines.size());
notif.box_h = line_count * line_h + (line_count - 1) * 1 + NOTIF_PADDING_V * 2;
notifications_.push_back(notif);
notifications.push_back(notif);
}
void toggleRenderInfo() { cycleRenderInfo(+1); }
@@ -418,47 +401,47 @@ namespace Overlay {
void setRenderInfoSegments(const char* s0, const char* s1, const char* s2, const char* s3, unsigned int mono_mask) {
const char* segs[INFO_SEGMENT_COUNT] = {s0, s1, s2, s3};
for (int i = 0; i < INFO_SEGMENT_COUNT; i++) {
info_segments_[i].mono_digits = (((mono_mask >> i) & 1U) != 0U);
info_segments[i].mono_digits = (((mono_mask >> i) & 1U) != 0U);
if (segs[i] != nullptr && *segs[i] != '\0') {
info_segments_[i].text = segs[i];
info_segments_[i].visible = true;
info_segments[i].text = segs[i];
info_segments[i].visible = true;
} else {
info_segments_[i].visible = false;
info_segments[i].visible = false;
}
}
}
void startCredits() {
if (credits_phase_ != CreditsPhase::IDLE) {
if (credits_phase != CreditsPhase::IDLE) {
return;
}
credits_phase_ = CreditsPhase::DELAY;
credits_timer_ = 0.0F;
credits_phase = CreditsPhase::DELAY;
credits_timer = 0.0F;
}
void cancelCredits() {
credits_phase_ = CreditsPhase::IDLE;
credits_timer_ = 0.0F;
notifications_.clear();
credits_phase = CreditsPhase::IDLE;
credits_timer = 0.0F;
notifications.clear();
}
auto creditsActive() -> bool {
return credits_phase_ != CreditsPhase::IDLE;
return credits_phase != CreditsPhase::IDLE;
}
auto isEscConsumed() -> bool {
return esc_waiting_;
return esc_waiting;
}
auto handleEscape() -> bool {
if (!esc_waiting_) {
if (!esc_waiting) {
// Primera pulsació: mostra avís i consumeix
esc_waiting_ = true;
esc_waiting = true;
showNotification(Locale::get("notifications.exit_double_esc"), 2.0F);
return true; // Consumit
}
// Segona pulsació: deixa passar
esc_waiting_ = false;
esc_waiting = false;
return false;
}
+29 -29
View File
@@ -56,18 +56,18 @@ namespace {
} // namespace
#endif // __EMSCRIPTEN__
std::unique_ptr<Screen> Screen::instance_;
std::unique_ptr<Screen> Screen::instance;
void Screen::init() {
instance_ = std::unique_ptr<Screen>(new Screen());
instance = std::unique_ptr<Screen>(new Screen());
}
void Screen::destroy() {
instance_.reset();
instance.reset();
}
auto Screen::get() -> Screen* {
return instance_.get();
return instance.get();
}
Screen::Screen() {
@@ -178,7 +178,7 @@ void Screen::initShaders() {
shader_backend_->setScalingMode(Options::video.scaling_mode);
shader_backend_->setVSync(Options::video.vsync);
shader_backend_->setTextureFilter(Options::video.texture_filter);
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
shader_backend_->setStretch43(Options::video.aspect_ratio_4_3);
shader_backend_->setDownscaleAlgo(Options::video.downscale_algo);
if (Options::video.supersampling) {
@@ -231,28 +231,28 @@ void Screen::present(Uint32* pixel_data) {
// no trencar la selecció de l'usuari.
Rendering::PostFXParams clean{};
shader_backend_->setPostFXParams(clean);
const auto prev_shader = shader_backend_->getActiveShader();
if (prev_shader != Rendering::ShaderType::POSTFX) {
const auto PREV_SHADER = shader_backend_->getActiveShader();
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
}
shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
shader_backend_->render();
if (prev_shader != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(prev_shader);
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(PREV_SHADER);
}
} else {
// Fallback SDL_Renderer. A mult=1, flux directe original: logical
// Fallback SDL_Renderer. A MULT=1, flux directe original: logical
// presentation (setada per applyFallbackPresentation) + scale mode de
// texture_ segons l'opció. A mult>1, la còpia intermèdia crea la
// texture_ segons l'opció. A MULT>1, la còpia intermèdia crea la
// font ampliada (NN via GPU), i es presenta via logical presentation
// a la mida de la font intermèdia.
SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32));
const int mult = Options::video.internal_resolution;
if (mult > 1) {
const int MULT = Options::video.internal_resolution;
if (MULT > 1) {
ensureFallbackInternalTexture();
if (internal_texture_sdl_ != nullptr) {
// Còpia NN a la textura intermèdia (mult·game). Sampler NN
// Còpia NN a la textura intermèdia (MULT·game). Sampler NN
// per construcció: volem píxels grans i nets.
SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST);
SDL_SetRenderTarget(renderer_, internal_texture_sdl_);
@@ -261,7 +261,7 @@ void Screen::present(Uint32* pixel_data) {
SDL_SetRenderTarget(renderer_, nullptr);
// Filtre global al pas final → finestra (via logical presentation
// que applyFallbackPresentation ja configura amb mida game·mult).
// que applyFallbackPresentation ja configura amb mida game·MULT).
SDL_ScaleMode final_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
@@ -273,9 +273,9 @@ void Screen::present(Uint32* pixel_data) {
}
// Si la creació de la textura intermèdia ha fallat, caiem al path normal.
}
// mult=1 (o fallback-del-fallback): texture_ directament. El scale mode
// MULT=1 (o fallback-del-fallback): texture_ directament. El scale mode
// el manté applyFallbackPresentation — però el re-apliquem per si la
// ruta mult>1 el va sobreescriure anteriorment.
// ruta MULT>1 el va sobreescriure anteriorment.
SDL_ScaleMode direct_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
@@ -346,7 +346,7 @@ auto Screen::toggleSupersampling() -> bool {
void Screen::toggleAspectRatio() {
Options::video.aspect_ratio_4_3 = !Options::video.aspect_ratio_4_3;
if (shader_backend_) {
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
shader_backend_->setStretch43(Options::video.aspect_ratio_4_3);
} else {
applyFallbackPresentation();
}
@@ -560,7 +560,7 @@ auto Screen::getActiveShaderName() const -> const char* {
}
void Screen::updateRenderInfo() {
static const Uint32 start_ticks = SDL_GetTicks();
static const Uint32 START_TICKS = SDL_GetTicks();
std::string driver = gpu_driver_.empty() ? "sdl" : toLower(gpu_driver_);
// Segment 0: FPS + driver (sempre visible)
@@ -578,7 +578,7 @@ void Screen::updateRenderInfo() {
// Segment 3: hora (només si show_time)
char time_buf[32] = {0};
if (Options::render_info.show_time) {
Uint32 elapsed = SDL_GetTicks() - start_ticks;
Uint32 elapsed = SDL_GetTicks() - START_TICKS;
int minutes = elapsed / 60000;
int seconds = (elapsed / 1000) % 60;
int centis = (elapsed / 10) % 100;
@@ -632,16 +632,16 @@ void Screen::applyFallbackPresentation() {
}
// Amb resolució interna N > 1, la mida lògica creix proporcionalment
// perquè SDL scale des de 320·N × 200·N a la finestra — menys aggressive linear.
const int mult = Options::video.internal_resolution < 1 ? 1 : Options::video.internal_resolution;
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH * mult, GAME_HEIGHT * mult, mode);
const int MULT = Options::video.internal_resolution < 1 ? 1 : Options::video.internal_resolution;
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH * MULT, GAME_HEIGHT * MULT, mode);
}
void Screen::ensureFallbackInternalTexture() {
if (renderer_ == nullptr) {
return;
}
const int mult = Options::video.internal_resolution;
if (mult <= 1) {
const int MULT = Options::video.internal_resolution;
if (MULT <= 1) {
// No cal textura intermèdia — recicla si la teníem.
if (internal_texture_sdl_ != nullptr) {
SDL_DestroyTexture(internal_texture_sdl_);
@@ -650,7 +650,7 @@ void Screen::ensureFallbackInternalTexture() {
}
return;
}
if (internal_texture_sdl_ != nullptr && internal_texture_mult_ == mult) {
if (internal_texture_sdl_ != nullptr && internal_texture_mult_ == MULT) {
return;
}
@@ -661,15 +661,15 @@ void Screen::ensureFallbackInternalTexture() {
internal_texture_sdl_ = SDL_CreateTexture(renderer_,
SDL_PIXELFORMAT_ABGR8888,
SDL_TEXTUREACCESS_TARGET,
GAME_WIDTH * mult,
GAME_HEIGHT * mult);
GAME_WIDTH * MULT,
GAME_HEIGHT * MULT);
if (internal_texture_sdl_ == nullptr) {
std::cerr << "Screen: failed to create fallback internal texture (×" << mult << "): "
std::cerr << "Screen: failed to create fallback internal texture (×" << MULT << "): "
<< SDL_GetError() << '\n';
internal_texture_mult_ = 0;
return;
}
internal_texture_mult_ = mult;
internal_texture_mult_ = MULT;
}
void Screen::adjustWindowSize() {
+1 -1
View File
@@ -71,7 +71,7 @@ class Screen {
void applyFallbackPresentation(); // Logical presentation + scale mode per al path SDL_Renderer
void ensureFallbackInternalTexture(); // Recrea internal_texture_sdl_ si cal (fallback path)
static std::unique_ptr<Screen> instance_;
static std::unique_ptr<Screen> instance;
SDL_Window* window_{nullptr};
SDL_Renderer* renderer_{nullptr};
@@ -906,8 +906,8 @@ namespace Rendering {
// ---- Calcular viewport (dimensions lògiques del canvas) ----
// Si 4:3 actiu, effective_height ja és 240 (la textura estirada)
const auto logical_w = static_cast<float>(game_width_);
const auto logical_h = static_cast<float>(effective_height);
const auto LOGICAL_W = static_cast<float>(game_width_);
const auto LOGICAL_H = static_cast<float>(effective_height);
float vx = 0.0F;
float vy = 0.0F;
@@ -916,8 +916,8 @@ namespace Rendering {
switch (scaling_mode_) {
case Options::ScalingMode::DISABLED:
// 1:1, sense escala (pot ser diminut en finestres grans)
vw = logical_w;
vh = logical_h;
vw = LOGICAL_W;
vh = LOGICAL_H;
break;
case Options::ScalingMode::STRETCH:
// Omple tota la finestra, escala no uniforme
@@ -925,23 +925,23 @@ namespace Rendering {
vh = static_cast<float>(sh);
break;
case Options::ScalingMode::LETTERBOX: {
const float SCALE = std::min(static_cast<float>(sw) / logical_w,
static_cast<float>(sh) / logical_h);
vw = logical_w * SCALE;
vh = logical_h * SCALE;
const float SCALE = std::min(static_cast<float>(sw) / LOGICAL_W,
static_cast<float>(sh) / LOGICAL_H);
vw = LOGICAL_W * SCALE;
vh = LOGICAL_H * SCALE;
break;
}
case Options::ScalingMode::OVERSCAN: {
const float SCALE = std::max(static_cast<float>(sw) / logical_w,
static_cast<float>(sh) / logical_h);
vw = logical_w * SCALE;
vh = logical_h * SCALE;
const float SCALE = std::max(static_cast<float>(sw) / LOGICAL_W,
static_cast<float>(sh) / LOGICAL_H);
vw = LOGICAL_W * SCALE;
vh = LOGICAL_H * SCALE;
break;
}
case Options::ScalingMode::INTEGER: {
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / static_cast<int>(logical_w), static_cast<int>(sh) / static_cast<int>(logical_h)));
vw = logical_w * static_cast<float>(SCALE);
vh = logical_h * static_cast<float>(SCALE);
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / static_cast<int>(LOGICAL_W), static_cast<int>(sh) / static_cast<int>(LOGICAL_H)));
vw = LOGICAL_W * static_cast<float>(SCALE);
vh = LOGICAL_H * static_cast<float>(SCALE);
break;
}
}
@@ -1286,7 +1286,7 @@ namespace Rendering {
}
}
void SDL3GPUShader::setStretch4_3(bool enabled) {
void SDL3GPUShader::setStretch43(bool enabled) {
stretch_4_3_ = enabled;
if (!is_initialized_ || device_ == nullptr) {
return;
@@ -118,8 +118,8 @@ namespace Rendering {
[[nodiscard]] auto getActiveShader() const -> ShaderType override { return active_shader_; }
// Estirament vertical 4:3 (fusionat amb l'upscale pass)
void setStretch4_3(bool enabled) override;
[[nodiscard]] auto isStretch4_3() const -> bool override { return stretch_4_3_; }
void setStretch43(bool enabled) override;
[[nodiscard]] auto isStretch43() const -> bool override { return stretch_4_3_; }
// Filtre de textura global (sempre aplicat, independent de 4:3)
void setTextureFilter(Options::TextureFilter filter) override {
+2 -2
View File
@@ -171,8 +171,8 @@ namespace Rendering {
* @brief Activa/desactiva estirament vertical 4:3 (200→240 línies efectives).
* Només afecta el viewport, no les textures ni els shaders.
*/
virtual void setStretch4_3(bool /*enabled*/) {}
[[nodiscard]] virtual auto isStretch4_3() const -> bool { return false; }
virtual void setStretch43(bool /*enabled*/) {}
[[nodiscard]] virtual auto isStretch43() const -> bool { return false; }
/**
* @brief Filtre de textura global per a l'upscale final (sempre aplicat).
+66 -177
View File
@@ -1,5 +1,6 @@
#include "core/rendering/text.hpp"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -12,6 +13,7 @@
// Forward declarations de gif.h (inclòs des de jdraw8.cpp, no es pot incloure dos vegades)
struct rgb;
// NOLINTNEXTLINE(readability-identifier-naming) — exportat per external/gif.h, no controlem el nom.
extern auto LoadGif(unsigned char* data, unsigned short* w, unsigned short* h) -> unsigned char*;
Text::Text(const char* fnt_file, const char* gif_file) {
@@ -157,60 +159,57 @@ void Text::loadBitmap(const char* gif_file) {
// --- Renderitzat ---
auto Text::resolveGlyph(uint32_t cp) const -> const GlyphInfo* {
auto it = glyphs_.find(cp);
if (it != glyphs_.end()) {
return &it->second;
}
it = glyphs_.find('?');
return (it != glyphs_.end()) ? &it->second : nullptr;
}
void Text::blitGlyph(Uint32* pixel_data, int dst_x, int dst_y, const GlyphInfo& glyph, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const {
const int GY_START = std::max(0, clip_y_min - dst_y);
const int GY_END = std::min(box_height_, clip_y_max - dst_y);
const int GX_START = std::max(0, clip_x_min - dst_x);
const int GX_END = std::min(glyph.w, clip_x_max - dst_x);
for (int gy = GY_START; gy < GY_END; gy++) {
const int SRC_Y = glyph.y + gy;
if (SRC_Y >= bitmap_height_) {
continue;
}
const int DST_ROW = dst_y + gy;
for (int gx = GX_START; gx < GX_END; gx++) {
const int SRC_X = glyph.x + gx;
if (SRC_X >= bitmap_width_) {
continue;
}
const Uint8 PIXEL = bitmap_[SRC_X + (SRC_Y * bitmap_width_)];
if (PIXEL != 0) {
pixel_data[(dst_x + gx) + (DST_ROW * SCREEN_WIDTH)] = color;
}
}
}
}
void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const {
if (bitmap_.empty() || (pixel_data == nullptr)) {
return;
}
const char* ptr = text;
int cursor_x = x;
while (*ptr != 0) {
uint32_t cp = nextCodepoint(ptr);
if (cp == 0) {
break;
}
auto it = glyphs_.find(cp);
if (it == glyphs_.end()) {
it = glyphs_.find('?');
if (it == glyphs_.end()) {
cursor_x += box_width_;
continue;
}
const GlyphInfo* glyph = resolveGlyph(cp);
if (glyph == nullptr) {
cursor_x += box_width_;
continue;
}
const auto& glyph = it->second;
// Pinta glifo pixel a pixel
for (int gy = 0; gy < box_height_; gy++) {
int dst_y = y + gy;
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
continue;
}
for (int gx = 0; gx < glyph.w; gx++) {
int dst_x = cursor_x + gx;
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
continue;
}
int src_x = glyph.x + gx;
int src_y = glyph.y + gy;
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
continue;
}
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
// Píxel no transparent (índex 0 és fons típicament)
if (pixel != 0) {
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
}
}
}
cursor_x += glyph.w + 1; // +1 kerning
blitGlyph(pixel_data, cursor_x, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
cursor_x += glyph->w + 1;
}
}
@@ -224,70 +223,29 @@ void Text::drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint3
if (bitmap_.empty() || (pixel_data == nullptr)) {
return;
}
// Descart ràpid si el glifo sencer cau fora verticalment
if (y + box_height_ <= clip_y_min || y >= clip_y_max) {
return;
}
const int X_MIN = std::max(0, clip_x_min);
const int X_MAX = std::min(SCREEN_WIDTH, clip_x_max);
const int Y_MIN = std::max(0, clip_y_min);
const int Y_MAX = std::min(SCREEN_HEIGHT, clip_y_max);
const char* ptr = text;
int cursor_x = x;
while (*ptr != 0) {
uint32_t cp = nextCodepoint(ptr);
if (cp == 0) {
break;
}
auto it = glyphs_.find(cp);
if (it == glyphs_.end()) {
it = glyphs_.find('?');
if (it == glyphs_.end()) {
cursor_x += box_width_;
continue;
}
}
const auto& glyph = it->second;
// Si el glifo està completament fora del clip horitzontal, salta
if (cursor_x + glyph.w <= clip_x_min || cursor_x >= clip_x_max) {
cursor_x += glyph.w + 1;
const GlyphInfo* glyph = resolveGlyph(cp);
if (glyph == nullptr) {
cursor_x += box_width_;
continue;
}
for (int gy = 0; gy < box_height_; gy++) {
int dst_y = y + gy;
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
continue;
}
if (dst_y < clip_y_min || dst_y >= clip_y_max) {
continue;
}
for (int gx = 0; gx < glyph.w; gx++) {
int dst_x = cursor_x + gx;
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
continue;
}
if (dst_x < clip_x_min || dst_x >= clip_x_max) {
continue;
}
int src_x = glyph.x + gx;
int src_y = glyph.y + gy;
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
continue;
}
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
if (pixel != 0) {
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
}
}
if (cursor_x + glyph->w > X_MIN && cursor_x < X_MAX) {
blitGlyph(pixel_data, cursor_x, y, *glyph, color, X_MIN, X_MAX, Y_MIN, Y_MAX);
}
cursor_x += glyph.w + 1;
cursor_x += glyph->w + 1;
}
}
@@ -295,54 +253,20 @@ void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 c
if (bitmap_.empty() || (pixel_data == nullptr)) {
return;
}
const char* ptr = text;
int cursor_x = x;
while (*ptr != 0) {
uint32_t cp = nextCodepoint(ptr);
if (cp == 0) {
break;
}
auto it = glyphs_.find(cp);
if (it == glyphs_.end()) {
it = glyphs_.find('?');
if (it == glyphs_.end()) {
cursor_x += cell_w;
continue;
}
const GlyphInfo* glyph = resolveGlyph(cp);
if (glyph == nullptr) {
cursor_x += cell_w;
continue;
}
const auto& glyph = it->second;
// Centra el glif dins la cel·la
int glyph_x = cursor_x + ((cell_w - glyph.w) / 2);
for (int gy = 0; gy < box_height_; gy++) {
int dst_y = y + gy;
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
continue;
}
for (int gx = 0; gx < glyph.w; gx++) {
int dst_x = glyph_x + gx;
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
continue;
}
int src_x = glyph.x + gx;
int src_y = glyph.y + gy;
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
continue;
}
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
if (pixel != 0) {
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
}
}
}
const int GLYPH_X = cursor_x + ((cell_w - glyph->w) / 2);
blitGlyph(pixel_data, GLYPH_X, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
cursor_x += cell_w;
}
}
@@ -351,62 +275,27 @@ void Text::drawMonoDigits(Uint32* pixel_data, int x, int y, const char* text, Ui
if (bitmap_.empty() || (pixel_data == nullptr)) {
return;
}
const char* ptr = text;
int cursor_x = x;
bool first = true;
while (*ptr != 0) {
uint32_t cp = nextCodepoint(ptr);
if (cp == 0) {
break;
}
auto it = glyphs_.find(cp);
if (it == glyphs_.end()) {
it = glyphs_.find('?');
if (it == glyphs_.end()) {
if (!first) {
cursor_x += 1;
}
cursor_x += box_width_;
first = false;
continue;
}
}
const auto& glyph = it->second;
bool is_digit = (cp >= '0' && cp <= '9');
if (!first) {
cursor_x += 1; // kerning
}
int glyph_x = is_digit ? cursor_x + ((digit_cell_w - glyph.w) / 2) : cursor_x;
for (int gy = 0; gy < box_height_; gy++) {
int dst_y = y + gy;
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
continue;
}
for (int gx = 0; gx < glyph.w; gx++) {
int dst_x = glyph_x + gx;
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
continue;
}
int src_x = glyph.x + gx;
int src_y = glyph.y + gy;
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
continue;
}
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
if (pixel != 0) {
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
}
}
const GlyphInfo* glyph = resolveGlyph(cp);
if (glyph == nullptr) {
cursor_x += box_width_;
first = false;
continue;
}
cursor_x += is_digit ? digit_cell_w : glyph.w;
const bool IS_DIGIT = (cp >= '0' && cp <= '9');
const int GLYPH_X = IS_DIGIT ? cursor_x + ((digit_cell_w - glyph->w) / 2) : cursor_x;
blitGlyph(pixel_data, GLYPH_X, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
cursor_x += IS_DIGIT ? digit_cell_w : glyph->w;
first = false;
}
}
+7
View File
@@ -57,6 +57,13 @@ class Text {
void loadFont(const char* fnt_file);
void loadBitmap(const char* gif_file);
// Resolt un codepoint al GlyphInfo corresponent o al fallback '?'.
// Retorna nullptr si ni el codepoint ni el fallback existeixen.
[[nodiscard]] auto resolveGlyph(uint32_t cp) const -> const GlyphInfo*;
// Pinta un glif a (dst_x, dst_y) amb clipping per finestra.
// Si la finestra és tota la pantalla, passar clip_x_min=0, clip_x_max=SCREEN_WIDTH, idem y.
void blitGlyph(Uint32* pixel_data, int dst_x, int dst_y, const GlyphInfo& glyph, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const;
static constexpr int SCREEN_WIDTH = 320;
static constexpr int SCREEN_HEIGHT = 200;
};
+51 -64
View File
@@ -15,8 +15,10 @@
// ni inline, així que no podem tornar-lo a incloure aquí. Ens fiem de les
// declaracions extern dels símbols que ens calen (linkatge C++ normal,
// igual que fa text.cpp).
// NOLINTBEGIN(readability-identifier-naming) — símbols externs de gif.h.
extern auto LoadGif(unsigned char* data, unsigned short* w, unsigned short* h) -> unsigned char*;
extern auto LoadPalette(unsigned char* data) -> unsigned char*;
// NOLINTEND(readability-identifier-naming)
namespace Resource {
@@ -33,7 +35,7 @@ namespace Resource {
}
} // namespace
auto Cache::getMusic(const std::string& name) -> JA_Music_t* {
auto Cache::getMusic(const std::string& name) -> Ja::Music* {
auto it = std::ranges::find_if(musics_, [&](const auto& m) { return m.name == name; });
if (it != musics_.end()) {
return it->music.get();
@@ -42,7 +44,7 @@ namespace Resource {
throw std::runtime_error("Music not found: " + name);
}
auto Cache::getSound(const std::string& name) -> JA_Sound_t* {
auto Cache::getSound(const std::string& name) -> Ja::Sound* {
auto it = std::ranges::find_if(sounds_, [&](const auto& s) { return s.name == name; });
if (it != sounds_.end()) {
return it->sound.get();
@@ -103,77 +105,62 @@ namespace Resource {
std::cout << "Resource::Cache: precarregant " << total_count_ << " assets\n";
}
void Cache::stepEachInList(List::Type type, const std::function<void()>& clear_fn, LoadStage next, const std::function<void(size_t)>& load_fn) {
auto items = List::get()->getListByType(type);
if (stage_index_ == 0) {
clear_fn();
}
if (stage_index_ >= items.size()) {
stage_ = next;
stage_index_ = 0;
return;
}
load_fn(stage_index_++);
}
void Cache::stepTextFiles() {
auto data_items = List::get()->getListByType(List::Type::DATA);
auto font_items = List::get()->getListByType(List::Type::FONT);
auto items = data_items;
items.insert(items.end(), font_items.begin(), font_items.end());
if (stage_index_ == 0) {
text_files_.clear();
}
if (stage_index_ >= items.size()) {
stage_ = LoadStage::DONE;
stage_index_ = 0;
std::cout << "Resource::Cache: precarrega completada (" << loaded_count_ << "/" << total_count_ << ")\n";
return;
}
loadOneTextFile(stage_index_++);
}
auto Cache::loadStep(int budget_ms) -> bool {
if (stage_ == LoadStage::DONE) {
return true;
}
const Uint64 start_ns = SDL_GetTicksNS();
const Uint64 budget_ns = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
const auto* list = List::get();
const Uint64 START_NS = SDL_GetTicksNS();
const Uint64 BUDGET_NS = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
while (stage_ != LoadStage::DONE) {
switch (stage_) {
case LoadStage::MUSICS: {
auto items = list->getListByType(List::Type::MUSIC);
if (stage_index_ == 0) {
musics_.clear();
}
if (stage_index_ >= items.size()) {
stage_ = LoadStage::SOUNDS;
stage_index_ = 0;
break;
}
loadOneMusic(stage_index_++);
case LoadStage::MUSICS:
stepEachInList(List::Type::MUSIC, [this] { musics_.clear(); }, LoadStage::SOUNDS, [this](size_t i) { loadOneMusic(i); });
break;
}
case LoadStage::SOUNDS: {
auto items = list->getListByType(List::Type::SOUND);
if (stage_index_ == 0) {
sounds_.clear();
}
if (stage_index_ >= items.size()) {
stage_ = LoadStage::BITMAPS;
stage_index_ = 0;
break;
}
loadOneSound(stage_index_++);
case LoadStage::SOUNDS:
stepEachInList(List::Type::SOUND, [this] { sounds_.clear(); }, LoadStage::BITMAPS, [this](size_t i) { loadOneSound(i); });
break;
}
case LoadStage::BITMAPS: {
auto items = list->getListByType(List::Type::BITMAP);
if (stage_index_ == 0) {
surfaces_.clear();
}
if (stage_index_ >= items.size()) {
stage_ = LoadStage::TEXT_FILES;
stage_index_ = 0;
break;
}
loadOneBitmap(stage_index_++);
case LoadStage::BITMAPS:
stepEachInList(List::Type::BITMAP, [this] { surfaces_.clear(); }, LoadStage::TEXT_FILES, [this](size_t i) { loadOneBitmap(i); });
break;
}
case LoadStage::TEXT_FILES: {
auto data_items = list->getListByType(List::Type::DATA);
auto font_items = list->getListByType(List::Type::FONT);
auto items = data_items;
items.insert(items.end(), font_items.begin(), font_items.end());
if (stage_index_ == 0) {
text_files_.clear();
}
if (stage_index_ >= items.size()) {
stage_ = LoadStage::DONE;
stage_index_ = 0;
std::cout << "Resource::Cache: precarrega completada (" << loaded_count_ << "/" << total_count_ << ")\n";
break;
}
loadOneTextFile(stage_index_++);
case LoadStage::TEXT_FILES:
stepTextFiles();
break;
}
case LoadStage::DONE:
break;
}
if ((SDL_GetTicksNS() - start_ns) >= budget_ns) {
if ((SDL_GetTicksNS() - START_NS) >= BUDGET_NS) {
break;
}
}
@@ -192,12 +179,12 @@ namespace Resource {
std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
return;
}
JA_Music_t* music = JA_LoadMusic(bytes.data(), static_cast<Uint32>(bytes.size()), path.c_str());
Ja::Music* music = Ja::loadMusic(bytes.data(), static_cast<Uint32>(bytes.size()), path.c_str());
if (music == nullptr) {
std::cerr << "Resource::Cache: JA_LoadMusic ha fallat per " << path << '\n';
std::cerr << "Resource::Cache: Ja::loadMusic ha fallat per " << path << '\n';
return;
}
musics_.push_back(MusicResource{.name = name, .music = std::unique_ptr<JA_Music_t, MusicDeleter>(music)});
musics_.push_back(MusicResource{.name = name, .music = std::unique_ptr<Ja::Music, MusicDeleter>(music)});
++loaded_count_;
std::cout << " [music ] " << name << '\n';
}
@@ -213,12 +200,12 @@ namespace Resource {
std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
return;
}
JA_Sound_t* sound = JA_LoadSound(bytes.data(), static_cast<uint32_t>(bytes.size()));
Ja::Sound* sound = Ja::loadSound(bytes.data(), static_cast<uint32_t>(bytes.size()));
if (sound == nullptr) {
std::cerr << "Resource::Cache: JA_LoadSound ha fallat per " << path << '\n';
std::cerr << "Resource::Cache: Ja::loadSound ha fallat per " << path << '\n';
return;
}
sounds_.push_back(SoundResource{.name = name, .sound = std::unique_ptr<JA_Sound_t, SoundDeleter>(sound)});
sounds_.push_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(sound)});
++loaded_count_;
std::cout << " [sound ] " << name << '\n';
}
+6 -2
View File
@@ -4,10 +4,12 @@
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "core/resources/resource_list.hpp"
#include "core/resources/resource_types.hpp"
namespace Resource {
@@ -27,8 +29,8 @@ namespace Resource {
auto operator=(const Cache&) -> Cache& = delete;
// Getters: throw runtime_error si el nom no existeix al cache.
auto getMusic(const std::string& name) -> JA_Music_t*;
auto getSound(const std::string& name) -> JA_Sound_t*;
auto getMusic(const std::string& name) -> Ja::Music*;
auto getSound(const std::string& name) -> Ja::Sound*;
auto getSurfacePixels(const std::string& name) -> const std::vector<Uint8>&;
auto getPaletteBytes(const std::string& name) -> const std::vector<Uint8>&;
auto getTextFile(const std::string& name) -> const std::vector<uint8_t>&;
@@ -56,6 +58,8 @@ namespace Resource {
void loadOneSound(size_t index);
void loadOneBitmap(size_t index);
void loadOneTextFile(size_t index);
void stepEachInList(List::Type type, const std::function<void()>& clear_fn, LoadStage next, const std::function<void(size_t)>& load_fn);
void stepTextFiles();
std::vector<MusicResource> musics_;
std::vector<SoundResource> sounds_;
+15 -15
View File
@@ -9,13 +9,13 @@
namespace ResourceHelper {
namespace {
ResourcePack pack_;
bool pack_loaded_ = false;
bool fallback_enabled_ = true;
ResourcePack pack_obj;
bool pack_loaded = false;
bool fallback_enabled = true;
auto readFromDisk(const std::string& relative_path) -> std::vector<uint8_t> {
const std::string full = std::string(file_getresourcefolder()) + relative_path;
std::ifstream file(full, std::ios::binary | std::ios::ate);
const std::string FULL = std::string(Jf::getResourceFolder()) + relative_path;
std::ifstream file(FULL, std::ios::binary | std::ios::ate);
if (!file) {
return {};
}
@@ -32,11 +32,11 @@ namespace ResourceHelper {
} // namespace
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool {
fallback_enabled_ = enable_fallback;
pack_loaded_ = pack_.loadPack(pack_file);
fallback_enabled = enable_fallback;
pack_loaded = pack_obj.loadPack(pack_file);
if (pack_loaded_) {
std::cout << "ResourceHelper: pack loaded (" << pack_.getResourceCount()
if (pack_loaded) {
std::cout << "ResourceHelper: pack loaded (" << pack_obj.getResourceCount()
<< " entries) from " << pack_file << '\n';
} else if (enable_fallback) {
std::cout << "ResourceHelper: no pack at " << pack_file
@@ -50,22 +50,22 @@ namespace ResourceHelper {
}
void shutdownResourceSystem() {
pack_.clear();
pack_loaded_ = false;
pack_obj.clear();
pack_loaded = false;
}
auto loadFile(const std::string& relative_path) -> std::vector<uint8_t> {
if (pack_loaded_ && pack_.hasResource(relative_path)) {
return pack_.getResource(relative_path);
if (pack_loaded && pack_obj.hasResource(relative_path)) {
return pack_obj.getResource(relative_path);
}
if (fallback_enabled_) {
if (fallback_enabled) {
return readFromDisk(relative_path);
}
return {};
}
auto hasPack() -> bool {
return pack_loaded_;
return pack_loaded;
}
} // namespace ResourceHelper
+1 -1
View File
@@ -5,7 +5,7 @@
#include <vector>
// API d'alt nivell per a llegir recursos. Prova primer el pack (si està
// carregat), després cau al fitxer solt dins `file_getresourcefolder()`
// carregat), després cau al fitxer solt dins `Jf::getResourceFolder()`
// si el fallback està activat.
namespace ResourceHelper {
+12 -11
View File
@@ -8,38 +8,39 @@
#include <vector>
// Forward declarations to keep this header light.
struct JA_Music_t;
struct JA_Sound_t;
void JA_DeleteMusic(JA_Music_t* music);
void JA_DeleteSound(JA_Sound_t* sound);
namespace Ja {
struct Music;
struct Sound;
void deleteMusic(Music* music);
void deleteSound(Sound* sound);
} // namespace Ja
namespace Resource {
struct MusicDeleter {
void operator()(JA_Music_t* music) const noexcept {
void operator()(Ja::Music* music) const noexcept {
if (music != nullptr) {
JA_DeleteMusic(music);
Ja::deleteMusic(music);
}
}
};
struct SoundDeleter {
void operator()(JA_Sound_t* sound) const noexcept {
void operator()(Ja::Sound* sound) const noexcept {
if (sound != nullptr) {
JA_DeleteSound(sound);
Ja::deleteSound(sound);
}
}
};
struct MusicResource {
std::string name;
std::unique_ptr<JA_Music_t, MusicDeleter> music;
std::unique_ptr<Ja::Music, MusicDeleter> music;
};
struct SoundResource {
std::string name;
std::unique_ptr<JA_Sound_t, SoundDeleter> sound;
std::unique_ptr<Ja::Sound, SoundDeleter> sound;
};
// Una entrada BITMAP descodifica un GIF i emmagatzema els seus
+181 -214
View File
@@ -32,95 +32,92 @@
#include "game/scenes/secreta_scene.hpp"
#include "game/scenes/slides_scene.hpp"
// Cheats del joc original — declarats a jinput.cpp
extern void JI_moveCheats(Uint8 new_key);
std::unique_ptr<Director> Director::instance_;
std::unique_ptr<Director> Director::instance;
Director::~Director() = default;
void Director::initGameContext() {
info::ctx.num_habitacio = Options::game.habitacio_inicial;
info::ctx.num_piramide = Options::game.piramide_inicial;
info::ctx.diners = Options::game.diners_inicial;
info::ctx.diamants = Options::game.diamants_inicial;
info::ctx.vida = Options::game.vides;
info::ctx.momies = 0;
info::ctx.nou_personatge = false;
info::ctx.pepe_activat = false;
Info::ctx.num_habitacio = Options::game.habitacio_inicial;
Info::ctx.num_piramide = Options::game.piramide_inicial;
Info::ctx.diners = Options::game.diners_inicial;
Info::ctx.diamants = Options::game.diamants_inicial;
Info::ctx.vida = Options::game.vides;
Info::ctx.momies = 0;
Info::ctx.nou_personatge = false;
Info::ctx.pepe_activat = false;
FILE* ini = fopen("trick.ini", "rb");
if (ini != nullptr) {
info::ctx.nou_personatge = true;
Info::ctx.nou_personatge = true;
fclose(ini);
}
}
auto Director::createNextScene() const -> std::unique_ptr<scenes::Scene> {
auto Director::createNextScene() const -> std::unique_ptr<Scenes::Scene> {
// Mentre el Resource::Cache no haja acabat de precarregar, executem
// el BootLoaderScene — pinta una barra de progrés i avança la
// càrrega per pressupost de temps. Quan acaba, retorna i tornem ací
// amb el cache plenament disponible per a la resta d'escenes.
if (Resource::Cache::get() != nullptr && !Resource::Cache::get()->isLoadDone()) {
return std::make_unique<scenes::BootLoaderScene>();
return std::make_unique<Scenes::BootLoaderScene>();
}
if (game_state_ == 0) {
// Gameplay. ModuleGame és una scenes::Scene des de la Phase A.
// Gameplay. ModuleGame és una Scenes::Scene des de la Phase A.
return std::make_unique<ModuleGame>();
}
// game_state_ == 1: dispatch al registry per num_piramide. Replica
// del redirect que el vell ModuleSequence::Go() feia: si el jugador
// arriba a la Secreta (6) sense prou diners, salta als slides de
// fracàs (7) abans de buscar l'escena al registry.
if (info::ctx.num_piramide == 6 && info::ctx.diners < 200) {
info::ctx.num_piramide = 7;
if (Info::ctx.num_piramide == 6 && Info::ctx.diners < 200) {
Info::ctx.num_piramide = 7;
}
return scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide);
return Scenes::SceneRegistry::instance().tryCreate(Info::ctx.num_piramide);
}
void Director::init() {
instance_ = std::unique_ptr<Director>(new Director());
instance = std::unique_ptr<Director>(new Director());
Gamepad::init();
// Registre d'escenes. Cada entrada = un state_key (`num_piramide`)
// amb una factory de `scenes::Scene`. iterate() consulta aquest
// amb una factory de `Scenes::Scene`. iterate() consulta aquest
// registry per a tots els states de seqüència (game_state_ == 1); si
// una clau no apareix ací, Director surt ordenadament.
auto& registry = scenes::SceneRegistry::instance();
registry.registerScene(0, [] { return std::make_unique<scenes::MenuScene>(); });
registry.registerScene(100, [] { return std::make_unique<scenes::MortScene>(); });
auto& registry = Scenes::SceneRegistry::instance();
registry.registerScene(0, [] { return std::make_unique<Scenes::MenuScene>(); });
registry.registerScene(100, [] { return std::make_unique<Scenes::MortScene>(); });
// BannerScene cobreix les piràmides 2..5 (el vell doBanner decideix
// pel switch intern llegint info::ctx.num_piramide).
// pel switch intern llegint Info::ctx.num_piramide).
for (int p = 2; p <= 5; ++p) {
registry.registerScene(p, [] { return std::make_unique<scenes::BannerScene>(); });
registry.registerScene(p, [] { return std::make_unique<Scenes::BannerScene>(); });
}
// SlidesScene cobreix els dos states on el vell `doSlides` s'invocava:
// - num_piramide == 1: slides narratius inicials (entrada al joc)
// - num_piramide == 7: slides de fracàs (ve del redirect 6→7 quan
// l'usuari no té prou diners per a la Secreta)
registry.registerScene(1, [] { return std::make_unique<scenes::SlidesScene>(); });
registry.registerScene(7, [] { return std::make_unique<scenes::SlidesScene>(); });
registry.registerScene(6, [] { return std::make_unique<scenes::SecretaScene>(); });
registry.registerScene(8, [] { return std::make_unique<scenes::CreditsScene>(); });
registry.registerScene(1, [] { return std::make_unique<Scenes::SlidesScene>(); });
registry.registerScene(7, [] { return std::make_unique<Scenes::SlidesScene>(); });
registry.registerScene(6, [] { return std::make_unique<Scenes::SecretaScene>(); });
registry.registerScene(8, [] { return std::make_unique<Scenes::CreditsScene>(); });
// State 255 (intro): dues variants segons `Options::game.use_new_logo`.
// La factory tria a runtime — així es pot togglar des del menú sense
// re-registrar. Les dues escenes construeixen una IntroSpritesScene
// com a sub-escena per a la part d'animacions de sprites.
registry.registerScene(255, []() -> std::unique_ptr<scenes::Scene> {
registry.registerScene(255, []() -> std::unique_ptr<Scenes::Scene> {
if (Options::game.use_new_logo) {
return std::make_unique<scenes::IntroNewLogoScene>();
return std::make_unique<Scenes::IntroNewLogoScene>();
}
return std::make_unique<scenes::IntroScene>();
return std::make_unique<Scenes::IntroScene>();
});
}
void Director::destroy() {
Gamepad::destroy();
instance_.reset();
instance.reset();
}
auto Director::get() -> Director* {
return instance_.get();
return instance.get();
}
void Director::togglePause() {
@@ -139,124 +136,104 @@ void Director::setup() {
has_frame_ = false;
}
void Director::applyRestart() {
restart_requested_ = false;
Audio::get()->stopMusic();
Audio::get()->stopAllSounds();
initGameContext();
Info::ctx.num_piramide = 255;
current_scene_.reset();
game_state_ = 1;
has_frame_ = false;
Menu::close();
Ji::setInputBlocked(false);
}
void Director::maybeStartTitleCredits() {
static bool credits_triggered_ = false;
if (credits_triggered_ || Info::ctx.num_piramide != 0) {
return;
}
if (Options::game.show_title_credits) {
Overlay::startCredits();
}
credits_triggered_ = true;
}
auto Director::tickActiveScene() -> bool {
if (current_scene_ && (current_scene_->done() || Jg::quitting())) {
game_state_ = current_scene_->nextState();
current_scene_.reset();
}
if (!current_scene_) {
if (game_state_ == -1 || Jg::quitting()) {
return false;
}
current_scene_ = createNextScene();
if (!current_scene_) {
return false;
}
current_scene_->onEnter();
last_tick_ms_ = SDL_GetTicks();
}
Ji::update();
const Uint32 NOW = SDL_GetTicks();
const int DELTA_MS = static_cast<int>(NOW - last_tick_ms_);
last_tick_ms_ = NOW;
current_scene_->tick(DELTA_MS);
Jd8::flip();
std::memcpy(game_frame_, Jd8::getFramebuffer(), sizeof(game_frame_));
has_frame_ = true;
return true;
}
auto Director::iterate() -> bool {
if (quit_requested_) {
JG_QuitSignal();
current_scene_.reset(); // destrueix l'escena actual ordenadament
Jg::quitSignal();
current_scene_.reset();
return false;
}
// Reinici "suau": processat al començament del frame per no manipular
// l'escena des d'una lambda del menú mentre encara s'està executant.
if (restart_requested_) {
restart_requested_ = false;
Audio::get()->stopMusic();
Audio::get()->stopAllSounds();
// Reinicialitza info::ctx des d'Options (vides, diners, diamants...)
// en lloc de ctx.reset() pla que deixaria vida=0 → jugador mort.
initGameContext();
// Força l'intro independentment de `piramide_inicial` (que pot estar
// configurat a una piràmide intermèdia per a proves ràpides).
info::ctx.num_piramide = 255;
current_scene_.reset();
game_state_ = 1; // 1 = dispatch via SceneRegistry per num_piramide
has_frame_ = false;
Menu::close();
JI_SetInputBlocked(false); // el menú ho havia bloquejat — cal desfer-ho
applyRestart();
}
if (!context_initialized_) {
initGameContext();
context_initialized_ = true;
}
constexpr Uint32 FRAME_MS_VSYNC = 16; // ~60 FPS amb VSync
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
const Uint32 frame_start = SDL_GetTicks();
constexpr Uint32 FRAME_MS_VSYNC = 16;
constexpr Uint32 FRAME_MS_NO_VSYNC = 4;
const Uint32 FRAME_START = SDL_GetTicks();
Gamepad::update();
KeyRemap::update();
GlobalInputs::handle();
Mouse::updateCursorVisibility();
// Bombeig de l'àudio: reomple l'stream de música i para els canals
// drenats. Substituïx el callback de SDL_AddTimer de la versió
// antiga — imprescindible per al port a emscripten.
Audio::update();
// Dispara els crèdits cinematogràfics la primera vegada que el joc
// arriba al menú del títol (info::ctx.num_piramide == 0).
static bool credits_triggered = false;
if (!credits_triggered && info::ctx.num_piramide == 0) {
if (Options::game.show_title_credits) {
Overlay::startCredits();
}
credits_triggered = true;
}
maybeStartTitleCredits();
// Si l'overlay ja no bloqueja ESC (timeout), desbloquegem
if (esc_blocked_ && !Overlay::isEscConsumed()) {
esc_blocked_ = false;
}
// Avança l'escena (si no estem pausats). En pausa, es manté l'escena
// congelada i re-presentem l'últim frame amb l'overlay fresc per
// damunt.
if (!paused_) {
// Transicions: si l'escena actual ha acabat (o s'ha senyalat
// quit), llegim el seu next state i la destruïm per crear la
// següent a continuació.
if (current_scene_ && (current_scene_->done() || JG_Quitting())) {
game_state_ = current_scene_->nextState();
current_scene_.reset();
if (!tickActiveScene()) {
return false;
}
// Si no hi ha escena activa, construeix la pròxima segons
// game_state_ i info::ctx. Si és impossible (game_state_ == -1,
// quit, o state no registrat), eixim del loop.
if (!current_scene_) {
if (game_state_ == -1 || JG_Quitting()) {
return false;
}
current_scene_ = createNextScene();
if (!current_scene_) {
return false;
}
current_scene_->onEnter();
last_tick_ms_ = SDL_GetTicks();
}
// Tick de l'escena. JI_Update refresca key_pressed/any_key; el
// delta_ms és el temps real transcorregut des de l'últim tick.
JI_Update();
const Uint32 now = SDL_GetTicks();
const int delta_ms = static_cast<int>(now - last_tick_ms_);
last_tick_ms_ = now;
current_scene_->tick(delta_ms);
// Converteix `screen` indexat → `pixel_data` ARGB amb la paleta
// actual. JD8_Flip ja no fa yield (Phase B.2 eliminà els fibers);
// ara només omple el framebuffer perquè el Director l'aprofite.
JD8_Flip();
std::memcpy(game_frame_, JD8_GetFramebuffer(), sizeof(game_frame_));
has_frame_ = true;
}
// Presenta sempre: parteix del frame net del joc, afegeix overlay i envia
if (has_frame_) {
std::memcpy(presentation_buffer_, game_frame_, sizeof(presentation_buffer_));
Screen::get()->present(presentation_buffer_);
}
// Límit de framerate segons VSync.
// Nota: quan el runtime posseïx el main loop (SDL_AppIterate /
// emscripten), aquest SDL_Delay no és ideal. Fase 7 afegirà un mode
// que es basa en el timing intern de SDL en lloc del delay explícit.
const Uint32 target_ms = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC;
const Uint32 elapsed = SDL_GetTicks() - frame_start;
if (elapsed < target_ms) {
SDL_Delay(target_ms - elapsed);
const Uint32 TARGET_MS = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC;
const Uint32 ELAPSED = SDL_GetTicks() - FRAME_START;
if (ELAPSED < TARGET_MS) {
SDL_Delay(TARGET_MS - ELAPSED);
}
return true;
@@ -266,7 +243,7 @@ void Director::teardown() {
// Senyal de quit i descàrrega ordenada de l'escena en curs. Els
// destructors de cada escena són no-bloquejants — ja no fan fades
// bloquejants. La resta de cleanup la gestiona `destroy()`.
JG_QuitSignal();
Jg::quitSignal();
current_scene_.reset();
}
@@ -288,111 +265,101 @@ void Director::pollAllEvents() {
}
}
auto Director::handleMenuEvent(const SDL_Event& event) -> bool {
// Empassar-se el KEY_UP d'una tecla que el menú va consumir en KEY_DOWN.
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 &&
event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) {
menu_keys_held_[event.key.scancode] = false;
return true;
}
const bool KEY_DOWN = event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat;
// Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot.
if (Menu::isCapturing() && KEY_DOWN) {
Menu::captureKey(event.key.scancode);
menu_keys_held_[event.key.scancode] = true;
return true;
}
// Pausa / menú toggle.
if (KEY_DOWN && event.key.scancode == KeyConfig::scancode("pause_toggle")) {
togglePause();
menu_keys_held_[event.key.scancode] = true;
return true;
}
if (KEY_DOWN && event.key.scancode == KeyConfig::scancode("menu_toggle")) {
Menu::toggle();
Ji::setInputBlocked(Menu::isOpen());
menu_keys_held_[event.key.scancode] = true;
return true;
}
// Si el menú està obert, consumeix tot l'input de teclat.
if (Menu::isOpen() && KEY_DOWN) {
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
Menu::close();
Ji::setInputBlocked(false);
esc_swallow_until_release_ = true;
} else {
Menu::handleKey(event.key.scancode);
if (!Menu::isOpen()) {
Ji::setInputBlocked(false);
}
}
menu_keys_held_[event.key.scancode] = true;
return true;
}
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
return true;
}
return false;
}
auto Director::handleEscapeEvent(const SDL_Event& event) -> bool {
// Salta els crèdits amb qualsevol tecla que arribe al joc.
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
Overlay::cancelCredits();
return true;
}
// Allibera el bloqueig d'ESC quan l'usuari la deixa anar.
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode == SDL_SCANCODE_ESCAPE && esc_swallow_until_release_) {
esc_swallow_until_release_ = false;
return true;
}
// ESC KEY_DOWN: bloqueja per polling i decideix notificació vs eixida.
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) {
esc_blocked_ = true;
if (!Overlay::isEscConsumed()) {
Overlay::handleEscape();
} else {
esc_blocked_ = false;
key_pressed_ = true;
Jg::quitSignal();
paused_ = false;
}
return true;
}
return false;
}
void Director::handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_QUIT) {
JG_QuitSignal();
Jg::quitSignal();
requestQuit();
}
// Hot-plug de gamepad (a Emscripten els dispositius web entren com
// JOYSTICK_ADDED/REMOVED perquè SDL no reconeix el GUID)
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED ||
event.type == SDL_EVENT_JOYSTICK_ADDED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
Gamepad::handleEvent(event);
return;
}
// Empassar-se el KEY_UP de qualsevol tecla que el menú va consumir en KEY_DOWN
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 &&
event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) {
menu_keys_held_[event.key.scancode] = false;
if (handleMenuEvent(event)) {
return;
}
// Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot
if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
Menu::captureKey(event.key.scancode);
menu_keys_held_[event.key.scancode] = true;
if (handleEscapeEvent(event)) {
return;
}
// Pausa: F11 (o tecla configurada) pausa/reprén la simulació.
// No mostrem notificació — l'indicador persistent "Pausa" a la cantonada
// superior dreta (pintat per Overlay) ja comunica l'estat.
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
event.key.scancode == KeyConfig::scancode("pause_toggle")) {
togglePause();
menu_keys_held_[event.key.scancode] = true;
return;
}
// Menú: F12 (o tecla configurada) obre/tanca el menú flotant
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
event.key.scancode == KeyConfig::scancode("menu_toggle")) {
Menu::toggle();
JI_SetInputBlocked(Menu::isOpen());
menu_keys_held_[event.key.scancode] = true;
return;
}
// Si el menú està obert, consumeix tot l'input de teclat
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
Menu::close();
JI_SetInputBlocked(false);
// Empassa l'ESC fins al release perquè el joc no la veja per polling
esc_swallow_until_release_ = true;
} else {
Menu::handleKey(event.key.scancode);
// El menú pot haver-se tancat (p.ex. Backspace al nivell arrel)
if (!Menu::isOpen()) {
JI_SetInputBlocked(false);
}
}
menu_keys_held_[event.key.scancode] = true;
return;
}
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
return; // no deixem passar KEY_UP al joc tampoc
}
// Salta els crèdits amb qualsevol tecla que arribe al joc. Es fa DESPRÉS
// del toggle del menú/pausa i del handling del menú obert — així F12 i
// SELECT (gamepad) obrin el menú sense cancel·lar els crèdits, i la
// navegació per dins del menú tampoc els anul·la.
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
Overlay::cancelCredits();
return;
}
// Allibera el bloqueig d'ESC quan l'usuari la deixa anar
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode == SDL_SCANCODE_ESCAPE && esc_swallow_until_release_) {
esc_swallow_until_release_ = false;
return;
}
// ESC: interceptem KEY_DOWN per bloquejar-la ABANS que el joc la veja per polling
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) {
esc_blocked_ = true; // Bloqueja ESC per polling immediatament
if (!Overlay::isEscConsumed()) {
// Primera pulsació: mostra notificació
Overlay::handleEscape();
} else {
// Segona pulsació: senyal d'eixida al joc
esc_blocked_ = false;
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode != SDL_SCANCODE_ESCAPE) {
const auto SC = event.key.scancode;
if (!KeyConfig::isGuiKey(SC)) {
key_pressed_ = true;
JG_QuitSignal();
// Si estem en pausa, la desactivem: el fiber del joc està
// congelat i necessita ser reprès per veure la senyal de
// quit i poder tornar de forma natural.
paused_ = false;
}
return; // no processa més aquest event
}
if (event.type == SDL_EVENT_KEY_UP) {
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
// Ja processat a KEY_DOWN, només deixem netejar el bloqueig
// quan l'overlay faça timeout
return;
} // Comprova si és una tecla d'UI registrada (no passa al joc).
// KeyConfig::isGuiKey cobreix totes les tecles GUI a la vegada,
// incloent pause_toggle i menu_toggle (defensa en profunditat:
// aquestes ja s'haurien hagut de menjar al swallow d'amunt).
const auto sc = event.key.scancode;
if (!KeyConfig::isGuiKey(sc)) {
key_pressed_ = true;
JI_moveCheats(sc);
Ji::moveCheats(SC);
}
}
Mouse::handleEvent(event);
@@ -400,7 +367,7 @@ void Director::handleEvent(const SDL_Event& event) {
void Director::requestQuit() {
quit_requested_ = true;
JG_QuitSignal();
Jg::quitSignal();
}
void Director::requestRestart() {
+19 -11
View File
@@ -9,8 +9,8 @@
#include "game/scenes/scene.hpp"
// El Director és l'únic thread del runtime. Cada iterate() fa input →
// tick de l'escena actual → JD8_Flip → overlay → present → sleep al frame
// target. Totes les escenes (`scenes::Scene` i `ModuleGame`) són
// tick de l'escena actual → Jd8::flip → overlay → present → sleep al frame
// target. Totes les escenes (`Scenes::Scene` i `ModuleGame`) són
// tick-based i no bloquegen — no hi ha fibers, mutex ni condition_variable.
// Compatible amb SDL_AppIterate i amb el futur port a emscripten.
class Director {
@@ -36,12 +36,12 @@ class Director {
void requestQuit();
[[nodiscard]] auto isQuitRequested() const -> bool { return quit_requested_; }
// Demana un reinici "suau": para música i sons, reseteja info::ctx i
// Demana un reinici "suau": para música i sons, reseteja Info::ctx i
// torna a l'intro (state 255). Es processa al començament del pròxim
// iterate() per evitar manipular l'escena des d'una lambda del menú.
void requestRestart();
// Consumeix el flag de "tecla polsada" (com l'antic JI_AnyKey)
// Consumeix el flag de "tecla polsada" (com l'antic Ji::anyKey)
auto consumeKeyPressed() -> bool;
// Indica si ESC està bloquejada (el joc no l'ha de veure)
@@ -57,16 +57,24 @@ class Director {
private:
Director() = default;
static std::unique_ptr<Director> instance_;
static std::unique_ptr<Director> instance;
void pollAllEvents(); // drenatge amb SDL_PollEvent, només per al bucle natiu
// Inicialitza info::ctx a partir de Options::game.* i comprova trick.ini.
// Inicialitza Info::ctx a partir de Options::game.* i comprova trick.ini.
// Es crida una sola vegada des d'iterate() a la primera invocació.
static void initGameContext();
// Construeix l'escena apropiada segons game_state_ i info::ctx.
// Construeix l'escena apropiada segons game_state_ i Info::ctx.
// Retorna nullptr si l'state actual no té escena registrada (bug).
[[nodiscard]] auto createNextScene() const -> std::unique_ptr<scenes::Scene>;
[[nodiscard]] auto createNextScene() const -> std::unique_ptr<Scenes::Scene>;
// Helpers d'iterate() — extrets per reduir complexitat cognitiva.
void applyRestart();
static void maybeStartTitleCredits();
auto tickActiveScene() -> bool; // true = continuar; false = sortir del loop
// Helpers d'handleEvent() — cada un retorna true si l'event s'ha consumit.
auto handleMenuEvent(const SDL_Event& event) -> bool;
auto handleEscapeEvent(const SDL_Event& event) -> bool;
// Buffers persistents entre iteracions. Abans eren locals a run(),
// ara són membres perquè iterate() els pot reutilitzar sense tornar-los
@@ -77,7 +85,7 @@ class Director {
// Estat de l'escena actual. Abans vivia al stack del GameFiber; des
// de la Phase B.2 de la migració viu directament al Director.
std::unique_ptr<scenes::Scene> current_scene_;
std::unique_ptr<Scenes::Scene> current_scene_;
int game_state_{1}; // 0 = gameplay (ModuleGame), 1 = via SceneRegistry, -1 = quit
Uint32 last_tick_ms_{0};
bool context_initialized_{false};
@@ -88,9 +96,9 @@ class Director {
std::atomic<bool> esc_blocked_{false};
std::atomic<bool> paused_{false};
// Quan el menú tanca amb ESC, empassem-nos l'ESC fins que l'usuari la deixe anar,
// per no fer eixir el joc al proper poll de JI_KeyPressed.
// per no fer eixir el joc al proper poll de Ji::keyPressed.
std::atomic<bool> esc_swallow_until_release_{false};
// Tecles consumides pel menú (KEY_DOWN): el KEY_UP associat cal empassar-lo
// per evitar que el joc (JI_AnyKey / JI_moveCheats) les veja quan el menú tanca.
// per evitar que el joc (Ji::anyKey / Ji::moveCheats) les veja quan el menú tanca.
bool menu_keys_held_[SDL_SCANCODE_COUNT]{};
};
+2
View File
@@ -1,3 +1,4 @@
// NOLINTBEGIN(clang-analyzer-unix.Malloc) — codi extern de tercers, no l'auditem
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -510,3 +511,4 @@ unsigned char* LoadGif(unsigned char *buffer, unsigned short* w, unsigned short*
fclose( gif_file );
}*/
// NOLINTEND(clang-analyzer-unix.Malloc)
+15 -15
View File
@@ -4,9 +4,9 @@
#include "core/jail/jgame.hpp"
Bola::Bola(JD8_Surface gfx, Prota* sam)
Bola::Bola(Jd8::Surface gfx, Prota* sam)
: Sprite(gfx) {
this->sam = sam;
this->sam_ = sam;
entitat.frames.reserve(2);
entitat.frames.push_back({30, 155, 15, 15});
@@ -17,28 +17,28 @@ Bola::Bola(JD8_Surface gfx, Prota* sam)
this->cur_frame = 0;
this->o = 0;
this->cycles_per_frame = 4;
this->cycles_per_frame_ = 4;
this->x = 20;
this->y = 100;
this->contador = 0;
this->contador_ = 0;
}
void Bola::draw() {
if (this->contador == 0) {
if (this->contador_ == 0) {
Sprite::draw();
}
}
void Bola::update() {
if (this->contador == 0) {
if (this->contador_ == 0) {
// Augmentem la x
this->x++;
if (this->x == 280) {
this->contador = 200;
this->contador_ = 200;
}
// Augmentem el frame
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
if (Jg::getCycleCounter() % this->cycles_per_frame_ == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
this->cur_frame = 0;
@@ -46,16 +46,16 @@ void Bola::update() {
}
// Comprovem si ha tocat a Sam
if (this->x > (this->sam->x - 7) && this->x < (this->sam->x + 7) && this->y > (this->sam->y - 7) && this->y < (this->sam->y + 7)) {
this->contador = 200;
info::ctx.vida--;
if (info::ctx.vida == 0) {
this->sam->o = 5;
if (this->x > (this->sam_->x - 7) && this->x < (this->sam_->x + 7) && this->y > (this->sam_->y - 7) && this->y < (this->sam_->y + 7)) {
this->contador_ = 200;
Info::ctx.vida--;
if (Info::ctx.vida == 0) {
this->sam_->o = 5;
}
}
} else {
this->contador--;
if (this->contador == 0) {
this->contador_--;
if (this->contador_ == 0) {
this->x = 20;
}
}
+3 -3
View File
@@ -6,12 +6,12 @@
class Bola : public Sprite {
public:
explicit Bola(JD8_Surface gfx, Prota* sam);
explicit Bola(Jd8::Surface gfx, Prota* sam);
void draw() override;
void update();
protected:
Uint8 contador;
Prota* sam;
Uint8 contador_;
Prota* sam_;
};
+6 -6
View File
@@ -4,7 +4,7 @@
#include "core/jail/jgame.hpp"
Engendro::Engendro(JD8_Surface gfx, Uint16 x, Uint16 y)
Engendro::Engendro(Jd8::Surface gfx, Uint16 x, Uint16 y)
: Sprite(gfx) {
entitat.frames.reserve(4);
for (int py = 50; py <= 65; py += 15) {
@@ -22,25 +22,25 @@ Engendro::Engendro(JD8_Surface gfx, Uint16 x, Uint16 y)
entitat.animacions[0].frames = {0, 1, 2, 3, 2, 1};
this->cur_frame = 0;
this->vida = 18;
this->vida_ = 18;
this->x = x;
this->y = y;
this->o = 0;
this->cycles_per_frame = 30;
this->cycles_per_frame_ = 30;
}
auto Engendro::update() -> bool {
bool mort = false;
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
if (Jg::getCycleCounter() % this->cycles_per_frame_ == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
this->cur_frame = 0;
}
this->vida--;
this->vida_--;
}
if (vida == 0) {
if (vida_ == 0) {
mort = true;
}
+2 -2
View File
@@ -4,10 +4,10 @@
class Engendro : public Sprite {
public:
explicit Engendro(JD8_Surface gfx, Uint16 x, Uint16 y);
explicit Engendro(Jd8::Surface gfx, Uint16 x, Uint16 y);
auto update() -> bool;
protected:
Uint8 vida;
Uint8 vida_;
};
+1 -1
View File
@@ -1,4 +1,4 @@
#include "game/info.hpp"
// La instància `info::ctx` està definida com a `inline` al header;
// La instància `Info::ctx` està definida com a `inline` al header;
// aquest fitxer es manté per a si cal afegir lògica addicional més endavant.
+3 -3
View File
@@ -1,6 +1,6 @@
#pragma once
namespace info {
namespace Info {
struct GameContext {
int num_piramide = 0;
@@ -17,8 +17,8 @@ namespace info {
};
// Instància única de l'estat del joc. Reemplaça les variables soltes del
// namespace `info::` per una struct encapsulada. A Fase 5 (single-threaded)
// namespace `Info::` per una struct encapsulada. A Fase 5 (single-threaded)
// es podrà passar per referència als mòduls en lloc d'accedir via singleton.
inline GameContext ctx;
} // namespace info
} // namespace Info
+87 -87
View File
@@ -6,126 +6,126 @@
#include "core/jail/jgame.hpp"
#include "core/jail/jinput.hpp"
Mapa::Mapa(JD8_Surface gfx, Prota* sam) {
this->gfx = gfx;
this->sam = sam;
Mapa::Mapa(Jd8::Surface gfx, Prota* sam) {
this->gfx_ = gfx;
this->sam_ = sam;
this->preparaFondoEstatic();
this->preparaTombes();
this->ultim_vertex.columna = 255;
this->frame_torxes = 0;
this->ultim_vertex_.columna = 255;
this->frame_torxes_ = 0;
this->farao = false;
this->clau = false;
this->porta_oberta = false;
this->nova_momia = false;
this->farao_ = false;
this->clau_ = false;
this->porta_oberta_ = false;
this->nova_momia_ = false;
}
Mapa::~Mapa() {
JD8_FreeSurface(this->fondo);
Jd8::freeSurface(this->fondo_);
}
void Mapa::draw() {
if (info::ctx.num_piramide != 4) {
switch (sam->o) {
if (Info::ctx.num_piramide != 4) {
switch (sam_->o) {
case 0: // Down
JD8_BlitCKToSurface(sam->x, sam->y, this->gfx, 15, 125 + sam->frame_pejades, 15, 1, this->fondo, 255);
Jd8::blitCKToSurface(sam_->x, sam_->y, this->gfx_, 15, 125 + sam_->frame_pejades, 15, 1, this->fondo_, 255);
break;
case 1: // Up
JD8_BlitCKToSurface(sam->x, sam->y + 15, this->gfx, 0, 125 + (14 - sam->frame_pejades), 15, 1, this->fondo, 255);
Jd8::blitCKToSurface(sam_->x, sam_->y + 15, this->gfx_, 0, 125 + (14 - sam_->frame_pejades), 15, 1, this->fondo_, 255);
break;
case 2: // Right
JD8_BlitCKToSurface(sam->x + 7, sam->y, this->gfx, 30 + sam->frame_pejades, 125, 1, 15, this->fondo, 255);
Jd8::blitCKToSurface(sam_->x + 7, sam_->y, this->gfx_, 30 + sam_->frame_pejades, 125, 1, 15, this->fondo_, 255);
break;
case 3: // Left
JD8_BlitCKToSurface(sam->x + 8, sam->y, this->gfx, 45 + (14 - sam->frame_pejades), 125, 1, 15, this->fondo, 255);
Jd8::blitCKToSurface(sam_->x + 8, sam_->y, this->gfx_, 45 + (14 - sam_->frame_pejades), 125, 1, 15, this->fondo_, 255);
break;
default:
break;
}
}
JD8_Blit(this->fondo);
Jd8::blit(this->fondo_);
// Pinta tombes
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
JD8_BlitCK(35 + (x * 65), 45 + (y * 35), this->gfx, this->tombes[x + (y * 4)].x, this->tombes[x + (y * 4)].y, 50, 20, 255);
Jd8::blitCK(35 + (x * 65), 45 + (y * 35), this->gfx_, this->tombes[x + (y * 4)].x, this->tombes[x + (y * 4)].y, 50, 20, 255);
}
}
JD8_BlitCK(45, 15, this->gfx, 30 + (this->frame_torxes * 25), 80, 25, 15, 255);
JD8_BlitCK(95, 15, this->gfx, 30 + (this->frame_torxes * 25), 80, 25, 15, 255);
JD8_BlitCK(195, 15, this->gfx, 30 + (this->frame_torxes * 25), 80, 25, 15, 255);
JD8_BlitCK(245, 15, this->gfx, 30 + (this->frame_torxes * 25), 80, 25, 15, 255);
Jd8::blitCK(45, 15, this->gfx_, 30 + (this->frame_torxes_ * 25), 80, 25, 15, 255);
Jd8::blitCK(95, 15, this->gfx_, 30 + (this->frame_torxes_ * 25), 80, 25, 15, 255);
Jd8::blitCK(195, 15, this->gfx_, 30 + (this->frame_torxes_ * 25), 80, 25, 15, 255);
Jd8::blitCK(245, 15, this->gfx_, 30 + (this->frame_torxes_ * 25), 80, 25, 15, 255);
};
void Mapa::update() {
if (((sam->x - 20) % 65 == 0) && ((sam->y - 30) % 35 == 0) && ((this->ultim_vertex.columna != (sam->x - 20) / 65) || (this->ultim_vertex.fila != (sam->y - 30) / 35))) {
this->vertex.columna = (sam->x - 20) / 65;
this->vertex.fila = (sam->y - 30) / 35;
if (this->ultim_vertex.columna != 255) {
if (((sam_->x - 20) % 65 == 0) && ((sam_->y - 30) % 35 == 0) && ((this->ultim_vertex_.columna != (sam_->x - 20) / 65) || (this->ultim_vertex_.fila != (sam_->y - 30) / 35))) {
this->vertex_.columna = (sam_->x - 20) / 65;
this->vertex_.fila = (sam_->y - 30) / 35;
if (this->ultim_vertex_.columna != 255) {
this->comprovaUltimCami();
}
this->ultim_vertex = this->vertex;
this->ultim_vertex_ = this->vertex_;
}
if (this->porta_oberta && sam->x == 150 && sam->y == 30) {
if (JI_KeyPressed(SDL_SCANCODE_UP)) {
this->sam->o = 4;
this->sam->y -= 15;
if (this->porta_oberta_ && sam_->x == 150 && sam_->y == 30) {
if (Ji::keyPressed(SDL_SCANCODE_UP)) {
this->sam_->o = 4;
this->sam_->y -= 15;
}
}
if (JG_GetCycleCounter() % 8 == 0) {
this->frame_torxes++;
this->frame_torxes = this->frame_torxes % 4;
if (Jg::getCycleCounter() % 8 == 0) {
this->frame_torxes_++;
this->frame_torxes_ = this->frame_torxes_ % 4;
}
}
auto Mapa::novaMomia() -> bool {
bool resultat = nova_momia;
nova_momia = false;
bool resultat = nova_momia_;
nova_momia_ = false;
return resultat;
}
void Mapa::preparaFondoEstatic() {
// Prepara el fondo esttic de l'habitaci
this->fondo = JD8_NewSurface();
if (info::ctx.num_piramide == 6) {
JD8_BlitToSurface(9, 2, this->gfx, 227, 185, 92, 7, this->fondo); // Text "SECRETA"
this->fondo_ = Jd8::newSurface();
if (Info::ctx.num_piramide == 6) {
Jd8::blitToSurface(9, 2, this->gfx_, 227, 185, 92, 7, this->fondo_); // Text "SECRETA"
} else {
JD8_BlitToSurface(9, 2, this->gfx, 60, 185, 39, 7, this->fondo); // Text "NIVELL"
JD8_BlitToSurface(72, 6, this->gfx, 153, 189, 3, 1, this->fondo); // Ralleta entre num piramide i num habitacio
Jd8::blitToSurface(9, 2, this->gfx_, 60, 185, 39, 7, this->fondo_); // Text "NIVELL"
Jd8::blitToSurface(72, 6, this->gfx_, 153, 189, 3, 1, this->fondo_); // Ralleta entre num piramide i num habitacio
}
JD8_BlitToSurface(130, 2, this->gfx, 225, 192, 19, 8, this->fondo); // Montonet de monedes + signe '='
JD8_BlitToSurface(220, 2, this->gfx, 160, 185, 48, 7, this->fondo); // Text "ENERGIA"
if (info::ctx.diners >= 200) {
JD8_BlitToSurface(175, 3, this->gfx, 60, 193, 7, 6, this->fondo);
Jd8::blitToSurface(130, 2, this->gfx_, 225, 192, 19, 8, this->fondo_); // Montonet de monedes + signe '='
Jd8::blitToSurface(220, 2, this->gfx_, 160, 185, 48, 7, this->fondo_); // Text "ENERGIA"
if (Info::ctx.diners >= 200) {
Jd8::blitToSurface(175, 3, this->gfx_, 60, 193, 7, 6, this->fondo_);
}
// Pinta taulells
for (int y = 0; y < 11; y++) {
for (int x = 0; x < 19; x++) {
switch (info::ctx.num_piramide) {
switch (Info::ctx.num_piramide) {
case 1:
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 0, 80, 15, 15, this->fondo);
Jd8::blitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx_, 0, 80, 15, 15, this->fondo_);
break;
case 2:
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 25, 95, 15, 15, this->fondo);
Jd8::blitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx_, 25, 95, 15, 15, this->fondo_);
break;
case 3:
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 40, 95, 15, 15, this->fondo);
Jd8::blitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx_, 40, 95, 15, 15, this->fondo_);
break;
case 4:
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 175 + ((rand() % 3) * 15), 80, 15, 15, this->fondo);
Jd8::blitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx_, 175 + ((rand() % 3) * 15), 80, 15, 15, this->fondo_);
break;
case 5:
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 130, 80, 15, 15, this->fondo);
Jd8::blitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx_, 130, 80, 15, 15, this->fondo_);
break;
case 6:
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 145, 80, 15, 15, this->fondo);
Jd8::blitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx_, 145, 80, 15, 15, this->fondo_);
break;
default:
break;
@@ -134,28 +134,28 @@ void Mapa::preparaFondoEstatic() {
}
// Pinta vores de les parets
JD8_BlitCKToSurface(5, 15, this->gfx, 30, 110, 15, 15, this->fondo, 255);
JD8_BlitCKToSurface(295, 15, this->gfx, 45, 110, 15, 15, this->fondo, 255);
JD8_BlitCKToSurface(5, 180, this->gfx, 0, 155, 15, 20, this->fondo, 255);
JD8_BlitCKToSurface(295, 180, this->gfx, 15, 155, 15, 20, this->fondo, 255);
Jd8::blitCKToSurface(5, 15, this->gfx_, 30, 110, 15, 15, this->fondo_, 255);
Jd8::blitCKToSurface(295, 15, this->gfx_, 45, 110, 15, 15, this->fondo_, 255);
Jd8::blitCKToSurface(5, 180, this->gfx_, 0, 155, 15, 20, this->fondo_, 255);
Jd8::blitCKToSurface(295, 180, this->gfx_, 15, 155, 15, 20, this->fondo_, 255);
// Pinta parets verticals
for (int i = 0; i < 10; i++) {
JD8_BlitToSurface(5, 30 + (i * 15), this->gfx, 0, 110, 15, 15, this->fondo);
JD8_BlitToSurface(295, 30 + (i * 15), this->gfx, 15, 110, 15, 15, this->fondo);
Jd8::blitToSurface(5, 30 + (i * 15), this->gfx_, 0, 110, 15, 15, this->fondo_);
Jd8::blitToSurface(295, 30 + (i * 15), this->gfx_, 15, 110, 15, 15, this->fondo_);
}
// Pinta parets hortzintals
for (int i = 0; i < 11; i++) {
JD8_BlitToSurface(20 + (i * 25), 185, this->gfx, 0, 95, 25, 15, this->fondo);
JD8_BlitToSurface(20 + (i * 25), 15, this->gfx, 0, 95, 25, 15, this->fondo);
Jd8::blitToSurface(20 + (i * 25), 185, this->gfx_, 0, 95, 25, 15, this->fondo_);
Jd8::blitToSurface(20 + (i * 25), 15, this->gfx_, 0, 95, 25, 15, this->fondo_);
}
// Pinta la porta
JD8_BlitCKToSurface(150, 18, this->gfx, 0, 143, 15, 12, this->fondo, 255);
Jd8::blitCKToSurface(150, 18, this->gfx_, 0, 143, 15, 12, this->fondo_, 255);
if (info::ctx.num_piramide == 2) {
JD8_BlitToSurface(5, 100, this->gfx, 30, 140, 15, 15, this->fondo);
if (Info::ctx.num_piramide == 2) {
Jd8::blitToSurface(5, 100, this->gfx_, 30, 140, 15, 15, this->fondo_);
}
}
@@ -166,12 +166,12 @@ void swap(Uint8& a, Uint8& b) noexcept {
}
void Mapa::preparaTombes() {
const Uint8 contingut = info::ctx.num_piramide == 6 ? CONTE_DIAMANT : CONTE_RES;
int cx = info::ctx.num_piramide == 6 ? 270 : 0;
int cy = info::ctx.num_piramide == 6 ? 50 : 0;
const Uint8 CONTINGUT = Info::ctx.num_piramide == 6 ? CONTE_DIAMANT : CONTE_RES;
int cx = Info::ctx.num_piramide == 6 ? 270 : 0;
int cy = Info::ctx.num_piramide == 6 ? 50 : 0;
for (auto& tombe : this->tombes) {
tombe.contingut = contingut;
tombe.contingut = CONTINGUT;
tombe.oberta = false;
tombe.costat[0] = false;
tombe.costat[1] = false;
@@ -180,7 +180,7 @@ void Mapa::preparaTombes() {
tombe.x = cx;
tombe.y = cy;
}
if (info::ctx.num_piramide == 6) {
if (Info::ctx.num_piramide == 6) {
return;
}
this->tombes[0].contingut = CONTE_FARAO;
@@ -204,12 +204,12 @@ auto minim(Uint8 a, Uint8 b) -> Uint8 {
}
void Mapa::comprovaUltimCami() {
Uint8 col_aux = abs(this->vertex.columna - this->ultim_vertex.columna);
Uint8 fil_aux = abs(this->vertex.fila - this->ultim_vertex.fila);
Uint8 col_aux = abs(this->vertex_.columna - this->ultim_vertex_.columna);
Uint8 fil_aux = abs(this->vertex_.fila - this->ultim_vertex_.fila);
if (col_aux > fil_aux) { // Cam horitzontal
Uint8 cami_fila = this->vertex.fila;
Uint8 cami_columna = minim(this->vertex.columna, this->ultim_vertex.columna);
Uint8 cami_fila = this->vertex_.fila;
Uint8 cami_columna = minim(this->vertex_.columna, this->ultim_vertex_.columna);
Sint8 caixa_avall = (cami_fila << 2) + cami_columna;
Sint8 caixa_amunt = caixa_avall - 4;
@@ -223,8 +223,8 @@ void Mapa::comprovaUltimCami() {
this->comprovaCaixa(caixa_amunt);
}
} else { // Cam vertical
Uint8 cami_columna = this->vertex.columna;
Uint8 cami_fila = minim(this->vertex.fila, this->ultim_vertex.fila);
Uint8 cami_columna = this->vertex_.columna;
Uint8 cami_fila = minim(this->vertex_.fila, this->ultim_vertex_.fila);
Sint8 caixa_dreta = (cami_fila << 2) + cami_columna;
Sint8 caixa_esquerra = caixa_dreta - 1;
@@ -247,7 +247,7 @@ void Mapa::comprovaCaixa(Uint8 num) {
}
// Si algun costat encara no està passat, no hi ha res que fer
if (std::any_of(std::begin(this->tombes[num].costat), std::end(this->tombes[num].costat), [](bool c) { return !c; })) {
if (std::ranges::any_of(this->tombes[num].costat, [](bool c) { return !c; })) {
return;
}
@@ -261,30 +261,30 @@ void Mapa::comprovaCaixa(Uint8 num) {
break;
case CONTE_TRESOR:
this->tombes[num].x = 100;
info::ctx.diners++;
Info::ctx.diners++;
break;
case CONTE_FARAO:
this->tombes[num].x = 150;
this->farao = true;
this->farao_ = true;
break;
case CONTE_CLAU:
this->tombes[num].x = 200;
this->clau = true;
this->clau_ = true;
break;
case CONTE_MOMIA:
this->tombes[num].y = 175;
this->nova_momia = true;
this->nova_momia_ = true;
break;
case CONTE_PERGAMI:
this->tombes[num].x = 250;
this->sam->pergami = true;
this->sam_->pergami = true;
break;
case CONTE_DIAMANT:
this->tombes[num].y = 70;
info::ctx.diamants++;
info::ctx.diners += VALOR_DIAMANT;
if (info::ctx.diamants == 16) {
this->farao = this->clau = true;
Info::ctx.diamants++;
Info::ctx.diners += VALOR_DIAMANT;
if (Info::ctx.diamants == 16) {
this->farao_ = this->clau_ = true;
}
break;
default:
@@ -295,8 +295,8 @@ void Mapa::comprovaCaixa(Uint8 num) {
}
void Mapa::comprovaPorta() {
if (this->clau && this->farao) {
JD8_BlitCKToSurface(150, 18, this->gfx, 15, 143, 15, 12, this->fondo, 255);
porta_oberta = true;
if (this->clau_ && this->farao_) {
Jd8::blitCKToSurface(150, 18, this->gfx_, 15, 143, 15, 12, this->fondo_, 255);
porta_oberta_ = true;
}
}
+11 -11
View File
@@ -31,7 +31,7 @@ struct Vertex {
class Mapa {
public:
explicit Mapa(JD8_Surface gfx, Prota* sam);
explicit Mapa(Jd8::Surface gfx, Prota* sam);
~Mapa();
Mapa(const Mapa&) = delete;
@@ -53,16 +53,16 @@ class Mapa {
void comprovaUltimCami();
void comprovaPorta();
JD8_Surface gfx;
JD8_Surface fondo;
Vertex vertex;
Vertex ultim_vertex;
Uint8 frame_torxes;
Jd8::Surface gfx_;
Jd8::Surface fondo_;
Vertex vertex_;
Vertex ultim_vertex_;
Uint8 frame_torxes_;
Prota* sam;
Prota* sam_;
bool farao;
bool clau;
bool porta_oberta;
bool nova_momia;
bool farao_;
bool clau_;
bool porta_oberta_;
bool nova_momia_;
};
+24 -24
View File
@@ -1,61 +1,61 @@
#include "game/marcador.hpp"
Marcador::Marcador(JD8_Surface gfx, Prota* sam) {
this->gfx = gfx;
this->sam = sam;
Marcador::Marcador(Jd8::Surface gfx, Prota* sam) {
this->gfx_ = gfx;
this->sam_ = sam;
}
void Marcador::draw() {
if (info::ctx.num_piramide < 6) {
this->pintaNumero(55, 2, info::ctx.num_piramide);
this->pintaNumero(80, 2, info::ctx.num_habitacio);
if (Info::ctx.num_piramide < 6) {
this->pintaNumero(55, 2, Info::ctx.num_piramide);
this->pintaNumero(80, 2, Info::ctx.num_habitacio);
}
this->pintaNumero(149, 2, info::ctx.diners / 100);
this->pintaNumero(156, 2, (info::ctx.diners % 100) / 10);
this->pintaNumero(163, 2, info::ctx.diners % 10);
this->pintaNumero(149, 2, Info::ctx.diners / 100);
this->pintaNumero(156, 2, (Info::ctx.diners % 100) / 10);
this->pintaNumero(163, 2, Info::ctx.diners % 10);
if (this->sam->pergami) {
JD8_BlitCK(190, 1, this->gfx, 209, 185, 15, 14, 255);
if (this->sam_->pergami) {
Jd8::blitCK(190, 1, this->gfx_, 209, 185, 15, 14, 255);
}
JD8_BlitCK(271, 1, this->gfx, 0, 20, 15, info::ctx.vida * 3, 255);
if (info::ctx.vida < 5) {
JD8_BlitCK(271, 1 + (info::ctx.vida * 3), this->gfx, 75, 20, 15, 15 - (info::ctx.vida * 3), 255);
Jd8::blitCK(271, 1, this->gfx_, 0, 20, 15, Info::ctx.vida * 3, 255);
if (Info::ctx.vida < 5) {
Jd8::blitCK(271, 1 + (Info::ctx.vida * 3), this->gfx_, 75, 20, 15, 15 - (Info::ctx.vida * 3), 255);
}
}
void Marcador::pintaNumero(Uint16 x, Uint16 y, Uint8 num) {
switch (num) {
case 0:
JD8_BlitCK(x, y, this->gfx, 141, 193, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 141, 193, 10, 7, 255);
break;
case 1:
JD8_BlitCK(x, y, this->gfx, 100, 185, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 100, 185, 10, 7, 255);
break;
case 2:
JD8_BlitCK(x, y, this->gfx, 110, 185, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 110, 185, 10, 7, 255);
break;
case 3:
JD8_BlitCK(x, y, this->gfx, 120, 185, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 120, 185, 10, 7, 255);
break;
case 4:
JD8_BlitCK(x, y, this->gfx, 130, 185, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 130, 185, 10, 7, 255);
break;
case 5:
JD8_BlitCK(x, y, this->gfx, 140, 185, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 140, 185, 10, 7, 255);
break;
case 6:
JD8_BlitCK(x, y, this->gfx, 101, 193, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 101, 193, 10, 7, 255);
break;
case 7:
JD8_BlitCK(x, y, this->gfx, 111, 193, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 111, 193, 10, 7, 255);
break;
case 8:
JD8_BlitCK(x, y, this->gfx, 121, 193, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 121, 193, 10, 7, 255);
break;
case 9:
JD8_BlitCK(x, y, this->gfx, 131, 193, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 131, 193, 10, 7, 255);
break;
default:
break;
+3 -3
View File
@@ -6,7 +6,7 @@
class Marcador {
public:
explicit Marcador(JD8_Surface gfx, Prota* sam);
explicit Marcador(Jd8::Surface gfx, Prota* sam);
~Marcador() = default;
void draw();
@@ -14,6 +14,6 @@ class Marcador {
protected:
void pintaNumero(Uint16 x, Uint16 y, Uint8 num);
JD8_Surface gfx;
Prota* sam;
Jd8::Surface gfx_;
Prota* sam_;
};
+76 -76
View File
@@ -8,38 +8,38 @@
#include "core/jail/jinput.hpp"
ModuleGame::ModuleGame() {
this->gfx = JD8_LoadSurface(info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif");
JG_SetUpdateTicks(10);
this->gfx_ = Jd8::loadSurface(Info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif");
Jg::setUpdateTicks(10);
this->sam = std::make_unique<Prota>(this->gfx);
this->mapa = std::make_unique<Mapa>(this->gfx, this->sam.get());
this->marcador = std::make_unique<Marcador>(this->gfx, this->sam.get());
if (info::ctx.num_piramide == 2) {
this->bola = std::make_unique<Bola>(this->gfx, this->sam.get());
this->sam_ = std::make_unique<Prota>(this->gfx_);
this->mapa_ = std::make_unique<Mapa>(this->gfx_, this->sam_.get());
this->marcador_ = std::make_unique<Marcador>(this->gfx_, this->sam_.get());
if (Info::ctx.num_piramide == 2) {
this->bola_ = std::make_unique<Bola>(this->gfx_, this->sam_.get());
}
this->iniciarMomies();
}
ModuleGame::~ModuleGame() {
JD8_FreeSurface(this->gfx);
Jd8::freeSurface(this->gfx_);
}
void ModuleGame::onEnter() {
// Primera Draw per omplir `screen` amb el contingut del gameplay
// abans que el fade-in arranque. Si no, les primeres iteracions del
// fade interpolarien cap a una paleta amb pantalla buida.
this->Draw();
this->draw();
// Audio::playMusic ja és idempotent: si la pista actual coincideix amb la
// demanada, no fa res. Per això podem cridar-lo cada onEnter sense
// desencadenar restarts indesitjats.
const char* music_name = "piramide_1_4_5.ogg";
if (info::ctx.num_piramide == 3) {
if (Info::ctx.num_piramide == 3) {
music_name = "piramide_3.ogg";
} else if (info::ctx.num_piramide == 2) {
} else if (Info::ctx.num_piramide == 2) {
music_name = "piramide_2.ogg";
} else if (info::ctx.num_piramide == 6) {
} else if (Info::ctx.num_piramide == 6) {
music_name = "secreta.ogg";
}
Audio::get()->playMusic(music_name);
@@ -47,33 +47,33 @@ void ModuleGame::onEnter() {
// Arranca el fade-in tick-based. El `PaletteFade` avança un pas (de
// 32) per cada tick; durant aquesta fase el gameplay no corre,
// només Draw+fade. Substituïx la crida bloquejant `JD8_FadeToPal`.
fade_.startFadeTo(JD8_LoadPalette(info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif"));
phase_ = Phase::FadingIn;
fade_.startFadeTo(Jd8::loadPalette(Info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif"));
phase_ = Phase::FADING_IN;
}
void ModuleGame::tick(int delta_ms) {
switch (phase_) {
case Phase::FadingIn:
case Phase::FADING_IN:
// No redibuixem durant el fade: el `screen` ja va ser omplit
// per la Draw() d'onEnter. Només el JD8_Flip del caller muta
// per la Draw() d'onEnter. Només el Jd8::flip del caller muta
// pixel_data segons la paleta que avança pas a pas.
fade_.tick(delta_ms);
if (fade_.done()) {
phase_ = Phase::Playing;
phase_ = Phase::PLAYING;
}
break;
case Phase::Playing:
this->Draw();
this->Update();
case Phase::PLAYING:
this->draw();
this->update();
if (this->final_ != 0) {
this->applyFinalTransitions();
fade_.startFadeOut();
phase_ = Phase::FadingOut;
phase_ = Phase::FADING_OUT;
}
break;
case Phase::FadingOut:
case Phase::FADING_OUT:
// No redibuixem: el `screen` té l'últim frame pintat per la
// fase Playing (just abans que Update() setegés `final_`).
// El vell `JD8_FadeOut` feia exactament això — flips amb
@@ -82,22 +82,22 @@ void ModuleGame::tick(int delta_ms) {
// "tornant" davant la porta després d'haver eixit).
fade_.tick(delta_ms);
if (fade_.done()) {
phase_ = Phase::Done;
phase_ = Phase::DONE;
}
break;
case Phase::Done:
case Phase::DONE:
break;
}
}
auto ModuleGame::nextState() const -> int {
if (JG_Quitting()) {
if (Jg::quitting()) {
return -1;
}
if (info::ctx.num_habitacio == 1 ||
info::ctx.num_piramide == 100 ||
info::ctx.num_piramide == 7) {
if (Info::ctx.num_habitacio == 1 ||
Info::ctx.num_piramide == 100 ||
Info::ctx.num_piramide == 7) {
return 1;
}
return 0;
@@ -105,87 +105,87 @@ auto ModuleGame::nextState() const -> int {
void ModuleGame::applyFinalTransitions() const {
if (this->final_ == 1) {
info::ctx.num_habitacio++;
if (info::ctx.num_habitacio == 6) {
info::ctx.num_habitacio = 1;
info::ctx.num_piramide++;
Info::ctx.num_habitacio++;
if (Info::ctx.num_habitacio == 6) {
Info::ctx.num_habitacio = 1;
Info::ctx.num_piramide++;
}
if (info::ctx.num_piramide == 6 && info::ctx.num_habitacio == 2) {
info::ctx.num_piramide++;
if (Info::ctx.num_piramide == 6 && Info::ctx.num_habitacio == 2) {
Info::ctx.num_piramide++;
}
} else if (this->final_ == 2) {
info::ctx.num_piramide = 100;
Info::ctx.num_piramide = 100;
}
}
void ModuleGame::Draw() {
// No crida JD8_Flip — el caller (mini-loop del fiber, o Director a
void ModuleGame::draw() {
// No crida Jd8::flip — el caller (mini-loop del fiber, o Director a
// Phase B.2) ho fa després de cada tick.
this->mapa->draw();
this->marcador->draw();
this->sam->draw();
for (auto& m : this->momies) {
this->mapa_->draw();
this->marcador_->draw();
this->sam_->draw();
for (auto& m : this->momies_) {
m->draw();
}
if (this->bola) {
this->bola->draw();
if (this->bola_) {
this->bola_->draw();
}
}
void ModuleGame::Update() {
if (JG_ShouldUpdate()) {
JI_Update();
void ModuleGame::update() {
if (Jg::shouldUpdate()) {
Ji::update();
this->final_ = this->sam->update();
const auto erased = std::erase_if(this->momies, [](auto& m) { return m->update(); });
info::ctx.momies -= static_cast<int>(erased);
if (this->bola) {
this->bola->update();
this->final_ = this->sam_->update();
const auto ERASED = std::erase_if(this->momies_, [](auto& m) { return m->update(); });
Info::ctx.momies -= static_cast<int>(ERASED);
if (this->bola_) {
this->bola_->update();
}
this->mapa->update();
if (this->mapa->novaMomia()) {
this->momies.emplace_back(std::make_unique<Momia>(this->gfx, true, 0, 0, this->sam.get()));
info::ctx.momies++;
this->mapa_->update();
if (this->mapa_->novaMomia()) {
this->momies_.emplace_back(std::make_unique<Momia>(this->gfx_, true, 0, 0, this->sam_.get()));
Info::ctx.momies++;
}
if (JI_CheatActivated("reviu")) {
info::ctx.vida = 5;
if (Ji::cheatActivated("reviu")) {
Info::ctx.vida = 5;
}
if (JI_CheatActivated("alone")) {
this->momies.clear();
info::ctx.momies = 0;
if (Ji::cheatActivated("alone")) {
this->momies_.clear();
Info::ctx.momies = 0;
}
if (JI_CheatActivated("obert")) {
if (Ji::cheatActivated("obert")) {
for (int i = 0; i < 16; i++) {
this->mapa->tombes[i].costat[0] = true;
this->mapa->tombes[i].costat[1] = true;
this->mapa->tombes[i].costat[2] = true;
this->mapa->tombes[i].costat[3] = true;
this->mapa->comprovaCaixa(i);
this->mapa_->tombes[i].costat[0] = true;
this->mapa_->tombes[i].costat[1] = true;
this->mapa_->tombes[i].costat[2] = true;
this->mapa_->tombes[i].costat[3] = true;
this->mapa_->comprovaCaixa(i);
}
}
if (JI_KeyPressed(SDL_SCANCODE_ESCAPE)) {
JG_QuitSignal();
if (Ji::keyPressed(SDL_SCANCODE_ESCAPE)) {
Jg::quitSignal();
}
}
}
void ModuleGame::iniciarMomies() {
if (info::ctx.num_habitacio == 1) {
info::ctx.momies = 1;
if (Info::ctx.num_habitacio == 1) {
Info::ctx.momies = 1;
} else {
info::ctx.momies++;
Info::ctx.momies++;
}
if (info::ctx.num_piramide == 6) {
info::ctx.momies = 8;
if (Info::ctx.num_piramide == 6) {
Info::ctx.momies = 8;
}
int x = 20;
int y = 170;
bool dimonis = info::ctx.num_piramide == 6;
for (int i = 0; i < info::ctx.momies; i++) {
this->momies.emplace_back(std::make_unique<Momia>(this->gfx, dimonis, x, y, this->sam.get()));
bool dimonis = Info::ctx.num_piramide == 6;
for (int i = 0; i < Info::ctx.momies; i++) {
this->momies_.emplace_back(std::make_unique<Momia>(this->gfx_, dimonis, x, y, this->sam_.get()));
x += 65;
if (x == 345) {
x = 20;
+22 -22
View File
@@ -14,20 +14,20 @@
#include "game/scenes/scene.hpp"
// Escena de gameplay pur. Reemplaça el vell `Go()` bloquejant amb
// l'interfície `scenes::Scene` tick-based: `onEnter()` arranca la
// l'interfície `Scenes::Scene` tick-based: `onEnter()` arranca la
// música i un fade-in, el `tick()` avança un frame (Draw + Update
// gated per JG_ShouldUpdate), i quan la partida acaba fa un fade-out
// gated per Jg::shouldUpdate), i quan la partida acaba fa un fade-out
// abans de retornar el next state.
//
// Tres fases internes:
// 1. FadingIn — fade-in 32 passos mentre el render segueix viu.
// 1. FADING_IN — fade-in 32 passos mentre el render segueix viu.
// 2. Playing — gameplay normal; `final_` es setja quan el prota mor
// o canvia de sala. `Update()` només avança cada 10 ms
// via `JG_ShouldUpdate` (ticker fix del jail).
// 3. FadingOut — fade-out 32 passos mantenint l'últim frame visible
// via `Jg::shouldUpdate` (ticker fix del jail).
// 3. FADING_OUT — fade-out 32 passos mantenint l'últim frame visible
// (substituïx el `JD8_FadeOut` bloquejant que feia el
// destructor legacy).
class ModuleGame : public scenes::Scene {
class ModuleGame : public Scenes::Scene {
public:
ModuleGame();
~ModuleGame() override;
@@ -39,31 +39,31 @@ class ModuleGame : public scenes::Scene {
void onEnter() override;
void tick(int delta_ms) override;
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::Done; }
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
[[nodiscard]] auto nextState() const -> int override;
private:
enum class Phase : std::uint8_t {
FadingIn,
Playing,
FadingOut,
Done,
FADING_IN,
PLAYING,
FADING_OUT,
DONE,
};
void Draw(); // render a `screen`; no crida JD8_Flip (ho fa el caller)
void Update(); // gated per JG_ShouldUpdate
void draw(); // render a `screen`; no crida Jd8::flip (ho fa el caller)
void update(); // gated per Jg::shouldUpdate
void iniciarMomies();
void applyFinalTransitions() const; // muta info::ctx quan final_ passa a !=0
void applyFinalTransitions() const; // muta Info::ctx quan final_ passa a !=0
Phase phase_{Phase::FadingIn};
scenes::PaletteFade fade_;
Phase phase_{Phase::FADING_IN};
Scenes::PaletteFade fade_;
Uint8 final_{0};
JD8_Surface gfx{nullptr};
Jd8::Surface gfx_{nullptr};
std::unique_ptr<Mapa> mapa;
std::unique_ptr<Prota> sam;
std::unique_ptr<Marcador> marcador;
std::vector<std::unique_ptr<Momia>> momies;
std::unique_ptr<Bola> bola;
std::unique_ptr<Mapa> mapa_;
std::unique_ptr<Prota> sam_;
std::unique_ptr<Marcador> marcador_;
std::vector<std::unique_ptr<Momia>> momies_;
std::unique_ptr<Bola> bola_;
};
+105 -92
View File
@@ -4,10 +4,10 @@
#include "core/jail/jgame.hpp"
Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
Momia::Momia(Jd8::Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
: Sprite(gfx) {
this->dimoni = dimoni;
this->sam = sam;
this->sam_ = sam;
entitat.frames.reserve(20);
for (int row = 0; row < 4; row++) {
@@ -15,7 +15,7 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
Frame f;
f.w = 15;
f.h = 15;
if (info::ctx.num_piramide == 4) {
if (Info::ctx.num_piramide == 4) {
f.h -= 5;
}
f.x = (col * 15) + 75;
@@ -43,7 +43,7 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
this->cur_frame = 0;
this->o = rand() % 4;
this->cycles_per_frame = 4;
this->cycles_per_frame_ = 4;
if (this->dimoni) {
if (x == 0) {
@@ -52,7 +52,7 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
this->x = x;
}
if (y == 0) {
if (this->sam->y > 100) {
if (this->sam_->y > 100) {
this->y = 30;
} else {
this->y = 170;
@@ -60,7 +60,7 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
} else {
this->y = y;
}
this->engendro = std::make_unique<Engendro>(gfx, this->x, this->y);
this->engendro_ = std::make_unique<Engendro>(gfx, this->x, this->y);
} else {
this->x = x;
this->y = y;
@@ -68,104 +68,117 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
}
void Momia::draw() {
if (this->engendro) {
this->engendro->draw();
if (this->engendro_) {
this->engendro_->draw();
} else {
Sprite::draw();
if (info::ctx.num_piramide == 4) {
if ((JG_GetCycleCounter() % 40) < 20) {
JD8_BlitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255);
if (Info::ctx.num_piramide == 4) {
if ((Jg::getCycleCounter() % 40) < 20) {
Jd8::blitCK(this->x, this->y, this->gfx_, 220, 80, 15, 15, 255);
} else {
JD8_BlitCK(this->x, this->y, this->gfx, 235, 80, 15, 15, 255);
Jd8::blitCK(this->x, this->y, this->gfx_, 235, 80, 15, 15, 255);
}
}
}
}
void Momia::pickHorizontalThenVertical() {
if (this->x > this->sam_->x) {
this->o = 3;
} else if (this->x < this->sam_->x) {
this->o = 2;
} else if (this->y < this->sam_->y) {
this->o = 0;
} else if (this->y > this->sam_->y) {
this->o = 1;
}
}
void Momia::pickVerticalThenHorizontal() {
if (this->y < this->sam_->y) {
this->o = 0;
} else if (this->y > this->sam_->y) {
this->o = 1;
} else if (this->x > this->sam_->x) {
this->o = 3;
} else if (this->x < this->sam_->x) {
this->o = 2;
}
}
void Momia::pickDirection() {
if (!this->dimoni) {
this->o = rand() % 4;
return;
}
if (rand() % 2 == 0) {
pickHorizontalThenVertical();
} else {
pickVerticalThenHorizontal();
}
}
void Momia::stepInDirection() {
switch (this->o) {
case 0:
if (y < 170) { this->y++; }
break;
case 1:
if (y > 30) { this->y--; }
break;
case 2:
if (x < 280) { this->x++; }
break;
case 3:
if (x > 20) { this->x--; }
break;
default:
break;
}
}
auto Momia::collidesWithSam() const -> bool {
return this->x > (this->sam_->x - 7) && this->x < (this->sam_->x + 7) &&
this->y > (this->sam_->y - 7) && this->y < (this->sam_->y + 7);
}
void Momia::applyCollisionWithSam() {
if (this->sam_->pergami) {
this->sam_->pergami = false;
return;
}
Info::ctx.vida--;
if (Info::ctx.vida == 0) {
this->sam_->o = 5;
}
}
auto Momia::update() -> bool {
bool morta = false;
if (this->engendro) {
if (this->engendro->update()) {
this->engendro.reset();
if (this->engendro_) {
if (this->engendro_->update()) {
this->engendro_.reset();
}
return morta;
return false;
}
if (this->sam->o < 4 && (this->dimoni || info::ctx.num_piramide == 5 || JG_GetCycleCounter() % 2 == 0)) {
if ((this->x - 20) % 65 == 0 && (this->y - 30) % 35 == 0) {
if (this->dimoni) {
if (rand() % 2 == 0) {
if (this->x > this->sam->x) {
this->o = 3;
} else if (this->x < this->sam->x) {
this->o = 2;
} else if (this->y < this->sam->y) {
this->o = 0;
} else if (this->y > this->sam->y) {
this->o = 1;
}
} else {
if (this->y < this->sam->y) {
this->o = 0;
} else if (this->y > this->sam->y) {
this->o = 1;
} else if (this->x > this->sam->x) {
this->o = 3;
} else if (this->x < this->sam->x) {
this->o = 2;
}
}
} else {
this->o = rand() % 4;
}
}
switch (this->o) {
case 0:
if (y < 170) {
this->y++;
}
break;
case 1:
if (y > 30) {
this->y--;
}
break;
case 2:
if (x < 280) {
this->x++;
}
break;
case 3:
if (x > 20) {
this->x--;
}
break;
default:
break;
}
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
this->cur_frame = 0;
}
}
if (this->x > (this->sam->x - 7) && this->x < (this->sam->x + 7) && this->y > (this->sam->y - 7) && this->y < (this->sam->y + 7)) {
morta = true;
if (this->sam->pergami) {
this->sam->pergami = false;
} else {
info::ctx.vida--;
if (info::ctx.vida == 0) {
this->sam->o = 5;
}
}
const bool SAM_ALIVE = this->sam_->o < 4;
const bool MAY_STEP = this->dimoni || Info::ctx.num_piramide == 5 || Jg::getCycleCounter() % 2 == 0;
if (!SAM_ALIVE || !MAY_STEP) {
return false;
}
if ((this->x - 20) % 65 == 0 && (this->y - 30) % 35 == 0) {
pickDirection();
}
stepInDirection();
if (Jg::getCycleCounter() % this->cycles_per_frame_ == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
this->cur_frame = 0;
}
}
return morta;
if (collidesWithSam()) {
applyCollisionWithSam();
return true;
}
return false;
}
+10 -3
View File
@@ -9,7 +9,7 @@
class Momia : public Sprite {
public:
explicit Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam);
explicit Momia(Jd8::Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam);
void draw() override;
auto update() -> bool;
@@ -17,6 +17,13 @@ class Momia : public Sprite {
bool dimoni;
protected:
Prota* sam;
std::unique_ptr<Engendro> engendro;
Prota* sam_;
std::unique_ptr<Engendro> engendro_;
void pickDirection();
void pickHorizontalThenVertical();
void pickVerticalThenHorizontal();
void stepInDirection();
[[nodiscard]] auto collidesWithSam() const -> bool;
void applyCollisionWithSam();
};
+27 -37
View File
@@ -437,7 +437,7 @@ namespace Options {
return true;
}
// --- Helper per a parsejar floats de YAML ---
// --- Helpers per a parsejar camps de YAML ---
static void parseFloatField(const fkyaml::node& node, const std::string& key, float& target) {
if (node.contains(key)) {
try {
@@ -448,6 +448,26 @@ namespace Options {
}
}
static void parseIntField(const fkyaml::node& node, const std::string& key, int& target) {
if (node.contains(key)) {
try {
target = node[key].get_value<int>();
} catch (...) {
// @INTENTIONAL: camp YAML no és int vàlid, mantenim valor per defecte.
}
}
}
static void parseBoolField(const fkyaml::node& node, const std::string& key, bool& target) {
if (node.contains(key)) {
try {
target = node[key].get_value<bool>();
} catch (...) {
// @INTENTIONAL: camp YAML no és bool vàlid, mantenim valor per defecte.
}
}
}
// --- Presets PostFX ---
void setPostFXFile(const std::string& path) { postfx_file_path = path; }
@@ -561,42 +581,12 @@ namespace Options {
parseFloatField(p, "mask_brightness", preset.mask_brightness);
parseFloatField(p, "curvature_x", preset.curvature_x);
parseFloatField(p, "curvature_y", preset.curvature_y);
if (p.contains("mask_type")) {
try {
preset.mask_type = p["mask_type"].get_value<int>();
} catch (...) { /* @INTENTIONAL: camp YAML invàlid, mantenim el valor per defecte. */
}
}
if (p.contains("enable_scanlines")) {
try {
preset.enable_scanlines = p["enable_scanlines"].get_value<bool>();
} catch (...) { /* @INTENTIONAL: camp YAML invàlid, mantenim el valor per defecte. */
}
}
if (p.contains("enable_multisample")) {
try {
preset.enable_multisample = p["enable_multisample"].get_value<bool>();
} catch (...) { /* @INTENTIONAL: camp YAML invàlid, mantenim el valor per defecte. */
}
}
if (p.contains("enable_gamma")) {
try {
preset.enable_gamma = p["enable_gamma"].get_value<bool>();
} catch (...) { /* @INTENTIONAL: camp YAML invàlid, mantenim el valor per defecte. */
}
}
if (p.contains("enable_curvature")) {
try {
preset.enable_curvature = p["enable_curvature"].get_value<bool>();
} catch (...) { /* @INTENTIONAL: camp YAML invàlid, mantenim el valor per defecte. */
}
}
if (p.contains("enable_sharper")) {
try {
preset.enable_sharper = p["enable_sharper"].get_value<bool>();
} catch (...) { /* @INTENTIONAL: camp YAML invàlid, mantenim el valor per defecte. */
}
}
parseIntField(p, "mask_type", preset.mask_type);
parseBoolField(p, "enable_scanlines", preset.enable_scanlines);
parseBoolField(p, "enable_multisample", preset.enable_multisample);
parseBoolField(p, "enable_gamma", preset.enable_gamma);
parseBoolField(p, "enable_curvature", preset.enable_curvature);
parseBoolField(p, "enable_sharper", preset.enable_sharper);
crtpi_presets.push_back(preset);
}
}
+134 -150
View File
@@ -5,84 +5,75 @@
#include "core/jail/jgame.hpp"
#include "core/jail/jinput.hpp"
Prota::Prota(JD8_Surface gfx)
: Sprite(gfx) {
entitat.frames.reserve(82);
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 5; x++) {
Frame f;
f.w = 15;
f.h = 15;
if (info::ctx.num_piramide == 4) {
f.h -= 5;
}
f.x = x * 15;
f.y = 20 + (y * 15);
entitat.frames.push_back(f);
}
namespace {
// Atura el frame.h a 10 quan piràmide 4 (sprite "petit" amb pijama de presoner).
auto adjustedHeight(int base_h) -> int {
return (Info::ctx.num_piramide == 4) ? base_h - 5 : base_h;
}
for (int y = 95; y < 185; y += 30) {
for (int x = 60; x < 315; x += 15) {
if (x != 300 || y != 155) {
Frame f;
f.w = 15;
f.h = 30;
if (info::ctx.num_piramide == 4) {
f.h -= 5;
void addFrameGrid(Entitat& entitat, int x0, int x1, int x_step, int y0, int y1, int y_step, int w, int h, int skip_x = -1, int skip_y = -1) {
for (int yy = y0; yy < y1; yy += y_step) {
for (int xx = x0; xx < x1; xx += x_step) {
if (xx == skip_x && yy == skip_y) {
continue;
}
f.x = x;
f.y = y;
Frame f;
f.w = w;
f.h = adjustedHeight(h);
f.x = xx;
f.y = yy;
entitat.frames.push_back(f);
}
}
}
for (int y = 20; y < 50; y += 15) {
for (int x = 225; x < 315; x += 15) {
Frame f;
f.w = 15;
f.h = 15;
if (info::ctx.num_piramide == 4) {
f.h -= 5;
}
f.x = x;
f.y = y;
entitat.frames.push_back(f);
void buildProtaFrames(Entitat& entitat) {
entitat.frames.reserve(82);
// Cara/quatre direccions (4×5 sprites de 15×15 a y=20..)
addFrameGrid(entitat, 0, 75, 15, 20, 80, 15, 15, 15);
// Animació de mort (15×30 a y=95..; salta x=300/y=155)
addFrameGrid(entitat, 60, 315, 15, 95, 185, 30, 15, 30, 300, 155);
// Animació de victòria (15×15 a y=20.., x=225..)
addFrameGrid(entitat, 225, 315, 15, 20, 50, 15, 15, 15);
}
void buildProtaAnimations(Entitat& entitat) {
entitat.animacions.resize(6);
for (int i = 0; i < 4; i++) {
entitat.animacions[i].frames = {
static_cast<Uint8>(0 + (i * 5)),
static_cast<Uint8>(1 + (i * 5)),
static_cast<Uint8>(2 + (i * 5)),
static_cast<Uint8>(1 + (i * 5)),
static_cast<Uint8>(0 + (i * 5)),
static_cast<Uint8>(3 + (i * 5)),
static_cast<Uint8>(4 + (i * 5)),
static_cast<Uint8>(3 + (i * 5)),
};
}
entitat.animacions[4].frames.resize(50);
for (int i = 0; i < 50; i++) {
entitat.animacions[4].frames[i] = i + 20;
}
entitat.animacions[5].frames.resize(48);
for (int i = 0; i < 12; i++) {
entitat.animacions[5].frames[i] = i + 70;
}
for (int i = 12; i < 48; i++) {
entitat.animacions[5].frames[i] = 81;
}
}
} // namespace
entitat.animacions.resize(6);
for (int i = 0; i < 4; i++) {
entitat.animacions[i].frames = {
static_cast<Uint8>(0 + (i * 5)),
static_cast<Uint8>(1 + (i * 5)),
static_cast<Uint8>(2 + (i * 5)),
static_cast<Uint8>(1 + (i * 5)),
static_cast<Uint8>(0 + (i * 5)),
static_cast<Uint8>(3 + (i * 5)),
static_cast<Uint8>(4 + (i * 5)),
static_cast<Uint8>(3 + (i * 5)),
};
}
entitat.animacions[4].frames.resize(50);
for (int i = 0; i < 50; i++) {
entitat.animacions[4].frames[i] = i + 20;
}
entitat.animacions[5].frames.resize(48);
for (int i = 0; i < 12; i++) {
entitat.animacions[5].frames[i] = i + 70;
}
for (int i = 12; i < 48; i++) {
entitat.animacions[5].frames[i] = 81;
}
Prota::Prota(Jd8::Surface gfx)
: Sprite(gfx) {
buildProtaFrames(entitat);
buildProtaAnimations(entitat);
cur_frame = 0;
x = 150;
y = 30;
o = 0;
cycles_per_frame = 4;
cycles_per_frame_ = 4;
pergami = false;
frame_pejades = 0;
}
@@ -90,96 +81,89 @@ Prota::Prota(JD8_Surface gfx)
void Prota::draw() {
Sprite::draw();
if (info::ctx.num_piramide == 4 && this->o != 4) {
if ((JG_GetCycleCounter() % 40) < 20) {
JD8_BlitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255);
if (Info::ctx.num_piramide == 4 && this->o != 4) {
if ((Jg::getCycleCounter() % 40) < 20) {
Jd8::blitCK(this->x, this->y, this->gfx_, 220, 80, 15, 15, 255);
} else {
JD8_BlitCK(this->x, this->y, this->gfx, 235, 80, 15, 15, 255);
Jd8::blitCK(this->x, this->y, this->gfx_, 235, 80, 15, 15, 255);
}
}
}
auto Prota::readDirection() -> Uint8 {
Uint8 dir = 4;
if (Ji::keyPressed(SDL_SCANCODE_DOWN)) {
if ((this->x - 20) % 65 == 0) { this->o = 0; }
dir = this->o;
}
if (Ji::keyPressed(SDL_SCANCODE_UP)) {
if ((this->x - 20) % 65 == 0) { this->o = 1; }
dir = this->o;
}
if (Ji::keyPressed(SDL_SCANCODE_RIGHT)) {
if ((this->y - 30) % 35 == 0) { this->o = 2; }
dir = this->o;
}
if (Ji::keyPressed(SDL_SCANCODE_LEFT)) {
if ((this->y - 30) % 35 == 0) { this->o = 3; }
dir = this->o;
}
return dir;
}
void Prota::stepInDirection(Uint8 dir) {
switch (dir) {
case 0:
if (this->y < 170) { this->y++; }
break;
case 1:
if (this->y > 30) { this->y--; }
break;
case 2:
if (this->x < 280) { this->x++; }
break;
case 3:
if (this->x > 20) { this->x--; }
break;
default:
break;
}
}
void Prota::advanceWalkingFrame(Uint8 dir) {
if (dir == 4) {
this->cur_frame = 0;
return;
}
this->frame_pejades++;
if (this->frame_pejades == 15) {
this->frame_pejades = 0;
}
if (Jg::getCycleCounter() % this->cycles_per_frame_ == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
this->cur_frame = 0;
}
}
}
auto Prota::advanceFinalAnimation() -> Uint8 {
if (Jg::getCycleCounter() % this->cycles_per_frame_ != 0) {
return 0;
}
this->cur_frame++;
if (this->cur_frame != entitat.animacions[this->o].frames.size()) {
return 0;
}
return (this->o == 4) ? 1 : 2;
}
auto Prota::update() -> Uint8 {
Uint8 eixir = 0;
if (this->o < 4) {
Uint8 dir = 4;
if (JI_KeyPressed(SDL_SCANCODE_DOWN)) {
if ((this->x - 20) % 65 == 0) {
this->o = 0;
}
dir = this->o;
}
if (JI_KeyPressed(SDL_SCANCODE_UP)) {
if ((this->x - 20) % 65 == 0) {
this->o = 1;
}
dir = this->o;
}
if (JI_KeyPressed(SDL_SCANCODE_RIGHT)) {
if ((this->y - 30) % 35 == 0) {
this->o = 2;
}
dir = this->o;
}
if (JI_KeyPressed(SDL_SCANCODE_LEFT)) {
if ((this->y - 30) % 35 == 0) {
this->o = 3;
}
dir = this->o;
}
switch (dir) {
case 0:
if (this->y < 170) {
this->y++;
}
break;
case 1:
if (this->y > 30) {
this->y--;
}
break;
case 2:
if (this->x < 280) {
this->x++;
}
break;
case 3:
if (this->x > 20) {
this->x--;
}
break;
default:
break;
}
if (dir == 4) {
this->cur_frame = 0;
} else {
this->frame_pejades++;
if (this->frame_pejades == 15) {
this->frame_pejades = 0;
}
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
this->cur_frame = 0;
}
}
}
eixir = 0U;
} else {
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
if (this->o == 4) {
eixir = 1;
} else {
eixir = 2;
}
}
}
if (this->o >= 4) {
return advanceFinalAnimation();
}
return eixir;
const Uint8 DIR = readDirection();
stepInDirection(DIR);
advanceWalkingFrame(DIR);
return 0;
}
+5 -1
View File
@@ -5,7 +5,7 @@
class Prota : public Sprite {
public:
explicit Prota(JD8_Surface gfx);
explicit Prota(Jd8::Surface gfx);
void draw() override;
auto update() -> Uint8;
@@ -14,4 +14,8 @@ class Prota : public Sprite {
bool pergami;
protected:
auto readDirection() -> Uint8;
void stepInDirection(Uint8 dir);
void advanceWalkingFrame(Uint8 dir);
auto advanceFinalAnimation() -> Uint8;
};
+21 -21
View File
@@ -8,47 +8,47 @@
#include "game/info.hpp"
#include "game/scenes/scene_utils.hpp"
namespace scenes {
namespace Scenes {
void BannerScene::onEnter() {
playMusic("music/banner.ogg");
gfx_ = SurfaceHandle("gfx/ffase.gif");
JD8_ClearScreen(0);
Jd8::clearScreen(0);
// Títols superior i inferior del banner (compartits per tots els nivells)
JD8_Blit(81, 24, gfx_, 81, 155, 168, 21);
JD8_Blit(39, 150, gfx_, 39, 175, 248, 20);
Jd8::blit(81, 24, gfx_, 81, 155, 168, 21);
Jd8::blit(39, 150, gfx_, 39, 175, 248, 20);
// Número de piràmide: les 4 variants del vell `doBanner` es reduïxen
// a coordenades (sx,sy) calculades a partir de l'índex 0..3.
const int idx = info::ctx.num_piramide - 2; // 2..5 → 0..3
if (idx >= 0 && idx <= 3) {
const int sx = (idx % 2) * 160;
const int sy = (idx / 2) * 75;
JD8_Blit(82, 60, gfx_, sx, sy, 160, 75);
// a coordenades (SX,SY) calculades a partir de l'índex 0..3.
const int IDX = Info::ctx.num_piramide - 2; // 2..5 → 0..3
if (IDX >= 0 && IDX <= 3) {
const int SX = (IDX % 2) * 160;
const int SY = (IDX / 2) * 75;
Jd8::blit(82, 60, gfx_, SX, SY, 160, 75);
}
// PaletteFade copia internament amb memcpy; alliberem la paleta temporal.
JD8_Palette pal = JD8_LoadPalette("gfx/ffase.gif");
Jd8::Palette pal = Jd8::loadPalette("gfx/ffase.gif");
fade_.startFadeTo(pal);
delete[] pal;
phase_ = Phase::FadingIn;
phase_ = Phase::FADING_IN;
remaining_ms_ = 5000;
}
void BannerScene::tick(int delta_ms) {
switch (phase_) {
case Phase::FadingIn:
case Phase::FADING_IN:
fade_.tick(delta_ms);
if (fade_.done()) {
phase_ = Phase::Showing;
phase_ = Phase::SHOWING;
}
break;
case Phase::Showing:
if (JI_AnyKey()) {
case Phase::SHOWING:
if (Ji::anyKey()) {
remaining_ms_ = 0;
} else {
remaining_ms_ -= delta_ms;
@@ -56,20 +56,20 @@ namespace scenes {
if (remaining_ms_ <= 0) {
Audio::get()->fadeOutMusic(250);
fade_.startFadeOut();
phase_ = Phase::FadingOut;
phase_ = Phase::FADING_OUT;
}
break;
case Phase::FadingOut:
case Phase::FADING_OUT:
fade_.tick(delta_ms);
if (fade_.done()) {
phase_ = Phase::Done;
phase_ = Phase::DONE;
}
break;
case Phase::Done:
case Phase::DONE:
break;
}
}
} // namespace scenes
} // namespace Scenes
+10 -10
View File
@@ -6,16 +6,16 @@
#include "game/scenes/scene.hpp"
#include "game/scenes/surface_handle.hpp"
namespace scenes {
namespace Scenes {
// Banner pre-piràmide ("PIRÀMIDE X"). Reemplaça `ModuleSequence::doBanner()`.
//
// Flux:
// 1. Arranca música "music/banner.ogg" i carrega gfx/ffase.gif.
// 2. Pinta títol, subtítol i número de piràmide segons info::ctx.num_piramide.
// 2. Pinta títol, subtítol i número de piràmide segons Info::ctx.num_piramide.
// 3. Fade-in de paleta.
// 4. Mostra ~5s o fins que es polse una tecla.
// 5. JA_FadeOutMusic(250) + fade-out de paleta.
// 5. Ja::fadeOutMusic(250) + fade-out de paleta.
// 6. Retorna nextState=0 per a entrar al ModuleGame.
//
// Registrat al SceneRegistry amb state_keys 2..5 (els num_piramide on
@@ -24,19 +24,19 @@ namespace scenes {
public:
void onEnter() override;
void tick(int delta_ms) override;
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::Done; }
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
[[nodiscard]] auto nextState() const -> int override { return 0; }
private:
enum class Phase : std::uint8_t { FadingIn,
Showing,
FadingOut,
Done };
enum class Phase : std::uint8_t { FADING_IN,
SHOWING,
FADING_OUT,
DONE };
SurfaceHandle gfx_;
PaletteFade fade_;
Phase phase_{Phase::FadingIn};
Phase phase_{Phase::FADING_IN};
int remaining_ms_{5000};
};
} // namespace scenes
} // namespace Scenes
+13 -13
View File
@@ -4,7 +4,7 @@
#include "core/resources/resource_cache.hpp"
#include "game/options.hpp"
namespace scenes {
namespace Scenes {
namespace {
constexpr int SCREEN_W = 320;
@@ -24,8 +24,8 @@ namespace scenes {
// Inicialitza la paleta mínima per a la barra. La resta de
// colors queden a negre — després cada escena del joc carregarà
// la seua pròpia paleta.
JD8_SetPaletteColor(BG_COLOR, 0, 0, 0);
JD8_SetPaletteColor(BAR_COLOR, 63, 63, 63);
Jd8::setPaletteColor(BG_COLOR, 0, 0, 0);
Jd8::setPaletteColor(BAR_COLOR, 63, 63, 63);
}
void BootLoaderScene::tick(int /*delta_ms*/) {
@@ -36,25 +36,25 @@ namespace scenes {
}
void BootLoaderScene::render() {
JD8_ClearScreen(BG_COLOR);
Jd8::clearScreen(BG_COLOR);
if (!Options::game.show_preload) {
return;
}
const float pct = Resource::Cache::get()->getProgress();
const int filled = static_cast<int>(static_cast<float>(BAR_W) * pct);
const float PCT = Resource::Cache::get()->getProgress();
const int FILLED = static_cast<int>(static_cast<float>(BAR_W) * PCT);
// Vora de la barra (línia 1 píxel a dalt i a baix).
JD8_FillRect(BAR_X - 1, BAR_Y - 1, BAR_W + 2, 1, BAR_COLOR);
JD8_FillRect(BAR_X - 1, BAR_Y + BAR_H, BAR_W + 2, 1, BAR_COLOR);
JD8_FillRect(BAR_X - 1, BAR_Y, 1, BAR_H, BAR_COLOR);
JD8_FillRect(BAR_X + BAR_W, BAR_Y, 1, BAR_H, BAR_COLOR);
Jd8::fillRect(BAR_X - 1, BAR_Y - 1, BAR_W + 2, 1, BAR_COLOR);
Jd8::fillRect(BAR_X - 1, BAR_Y + BAR_H, BAR_W + 2, 1, BAR_COLOR);
Jd8::fillRect(BAR_X - 1, BAR_Y, 1, BAR_H, BAR_COLOR);
Jd8::fillRect(BAR_X + BAR_W, BAR_Y, 1, BAR_H, BAR_COLOR);
// Ompliment proporcional al progrés.
if (filled > 0) {
JD8_FillRect(BAR_X, BAR_Y, filled, BAR_H, BAR_COLOR);
if (FILLED > 0) {
Jd8::fillRect(BAR_X, BAR_Y, FILLED, BAR_H, BAR_COLOR);
}
}
} // namespace scenes
} // namespace Scenes
+2 -2
View File
@@ -2,7 +2,7 @@
#include "game/scenes/scene.hpp"
namespace scenes {
namespace Scenes {
// Escena de boot que conduix la càrrega incremental del Resource::Cache.
// tick() crida loadStep amb un pressupost de ~8ms i pinta una barra
@@ -23,4 +23,4 @@ namespace scenes {
bool done_{false};
};
} // namespace scenes
} // namespace Scenes
+30 -30
View File
@@ -29,12 +29,12 @@ namespace {
};
constexpr int CONTADOR_MAX = 3100; // ~62 s de crèdits a 20 ms/tick
constexpr int TICK_MS = 20; // JG_SetUpdateTicks heretat del doSlides previ
constexpr int TICK_MS = 20; // Jg::setUpdateTicks heretat del doSlides previ
constexpr int BG_INDEX = 255;
} // namespace
namespace scenes {
namespace Scenes {
// No toquem la paleta activa: SetScreenPalette n'ha pres ownership.
CreditsScene::~CreditsScene() = default;
@@ -44,7 +44,7 @@ namespace scenes {
// previ ("music/final.ogg"). Si l'escena s'arrenca directament (test
// amb piramide_inicial=8) no hi ha res que heretar, així que
// arranquem la mateixa pista només si no sona res. Inocu en el
// flux normal: JA_MUSIC_PLAYING fa que no la tornem a tocar.
// flux normal: Ja::MusicState::PLAYING fa que no la tornem a tocar.
if (Audio::getRealMusicState() != Audio::MusicState::PLAYING) {
playMusic("music/final.ogg");
}
@@ -52,50 +52,50 @@ namespace scenes {
vaddr2_ = SurfaceHandle("gfx/final.gif");
vaddr3_ = SurfaceHandle("gfx/finals.gif");
JD8_Palette pal = JD8_LoadPalette("gfx/final.gif");
JD8_SetScreenPalette(pal);
Jd8::Palette pal = Jd8::loadPalette("gfx/final.gif");
Jd8::setScreenPalette(pal);
// `pal` passa a ser propietat de main_palette — no l'alliberem.
phase_ = Phase::Rolling;
phase_ = Phase::ROLLING;
contador_ = 1;
contador_acc_ms_ = 0;
}
void CreditsScene::render() {
JD8_ClearScreen(BG_INDEX);
Jd8::clearScreen(BG_INDEX);
// Columna 1: scroll vertical del bloc (0,0,80,200) pujant des de
// y=200 fins que el contador supera 2750.
if (contador_ < 2750) {
JD8_BlitCKCut(115, 200 - (contador_ / 6), vaddr2_, 0, 0, 80, 200, 0);
Jd8::blitCKCut(115, 200 - (contador_ / 6), vaddr2_, 0, 0, 80, 200, 0);
}
// Columna 2: scroll vertical del bloc (85,0,120,140), arrenca
// a contador 1200 i s'atura (fix en y=20) a partir de 2250.
if ((contador_ > 1200) && (contador_ < 2280)) {
JD8_BlitCKCut(100, 200 - ((contador_ - 1200) / 6), vaddr2_, 85, 0, 120, 140, 0);
Jd8::blitCKCut(100, 200 - ((contador_ - 1200) / 6), vaddr2_, 85, 0, 120, 140, 0);
} else if (contador_ >= 2250) {
JD8_BlitCK(100, 20, vaddr2_, 85, 0, 120, 140, 0);
Jd8::blitCK(100, 20, vaddr2_, 85, 0, 120, 140, 0);
}
// Fons: 4 capes parallax + cotxe només si l'usuari ha aconseguit
// tots els diamants (final "bo"). Altrament fons estàtic.
if (info::ctx.diamants == 16) {
JD8_BlitCKScroll(50, vaddr3_, ((contador_ >> 3) % 320) + 1, 0, 50, 255);
JD8_BlitCKScroll(50, vaddr3_, ((contador_ >> 2) % 320) + 1, 50, 50, 255);
JD8_BlitCKScroll(50, vaddr3_, ((contador_ >> 1) % 320) + 1, 100, 50, 255);
JD8_BlitCKScroll(50, vaddr3_, (contador_ % 320) + 1, 150, 50, 255);
if (Info::ctx.diamants == 16) {
Jd8::blitCKScroll(50, vaddr3_, ((contador_ >> 3) % 320) + 1, 0, 50, 255);
Jd8::blitCKScroll(50, vaddr3_, ((contador_ >> 2) % 320) + 1, 50, 50, 255);
Jd8::blitCKScroll(50, vaddr3_, ((contador_ >> 1) % 320) + 1, 100, 50, 255);
Jd8::blitCKScroll(50, vaddr3_, (contador_ % 320) + 1, 150, 50, 255);
const CocheFrame& cf = COCHE_FRAMES[coche_.frame()];
JD8_BlitCK(100, 50, vaddr2_, cf.x, cf.y, 106, 48, 255);
Jd8::blitCK(100, 50, vaddr2_, cf.x, cf.y, 106, 48, 255);
} else {
JD8_BlitCK(0, 50, vaddr3_, 0, 0, 320, 50, 255);
JD8_BlitCK(0, 50, vaddr3_, 0, 50, 320, 50, 255);
Jd8::blitCK(0, 50, vaddr3_, 0, 0, 320, 50, 255);
Jd8::blitCK(0, 50, vaddr3_, 0, 50, 320, 50, 255);
}
// Barres de marc que cobreixen els extrems del scroll vertical.
JD8_FillSquare(0, 50, BG_INDEX);
JD8_FillSquare(100, 10, BG_INDEX);
Jd8::fillSquare(0, 50, BG_INDEX);
Jd8::fillSquare(100, 10, BG_INDEX);
}
void CreditsScene::writeTrickIni() {
@@ -104,14 +104,14 @@ namespace scenes {
std::fwrite("1", 1, 1, ini);
std::fclose(ini);
}
info::ctx.nou_personatge = true;
Info::ctx.nou_personatge = true;
}
void CreditsScene::tick(int delta_ms) {
switch (phase_) {
case Phase::Rolling: {
case Phase::ROLLING: {
// Avancem el contador en passos discrets de 20 ms, igual
// que feia JG_ShouldUpdate(20) al vell doCredits.
// que feia Jg::shouldUpdate(20) al vell doCredits.
contador_acc_ms_ += delta_ms;
while (contador_acc_ms_ >= TICK_MS) {
contador_acc_ms_ -= TICK_MS;
@@ -121,25 +121,25 @@ namespace scenes {
coche_.tick(delta_ms);
render();
if (JI_AnyKey() || contador_ >= CONTADOR_MAX) {
if (Ji::anyKey() || contador_ >= CONTADOR_MAX) {
writeTrickIni();
fade_.startFadeOut();
phase_ = Phase::FadingOut;
phase_ = Phase::FADING_OUT;
}
break;
}
case Phase::FadingOut:
case Phase::FADING_OUT:
fade_.tick(delta_ms);
if (fade_.done()) {
info::ctx.num_piramide = 255;
phase_ = Phase::Done;
Info::ctx.num_piramide = 255;
phase_ = Phase::DONE;
}
break;
case Phase::Done:
case Phase::DONE:
break;
}
}
} // namespace scenes
} // namespace Scenes
+9 -9
View File
@@ -8,7 +8,7 @@
#include "game/scenes/scene.hpp"
#include "game/scenes/surface_handle.hpp"
namespace scenes {
namespace Scenes {
// Crèdits finals del joc. Reemplaça `ModuleSequence::doCredits()`.
//
@@ -16,10 +16,10 @@ namespace scenes {
// 1. Carrega gfx/final.gif (sprites de crèdits) i gfx/finals.gif (fons).
// 2. Mostra els crèdits amb scroll vertical de 2 columnes durant
// ~62 segons (contador 0..3100 × 20 ms).
// 3. Si `info::ctx.diamants == 16`, pinta addicionalment un parallax
// 3. Si `Info::ctx.diamants == 16`, pinta addicionalment un parallax
// de 4 capes amb cotxe animat (8 frames). Si no, 2 blits fixos.
// 4. Al acabar (per tecla o per contador), crea el fitxer `trick.ini`
// i activa `info::ctx.nou_personatge`.
// i activa `Info::ctx.nou_personatge`.
// 5. Fade-out de paleta. Torna a la intro (num_piramide = 255).
//
// Registrada al SceneRegistry amb state_key = 8.
@@ -30,12 +30,12 @@ namespace scenes {
void onEnter() override;
void tick(int delta_ms) override;
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::Done; }
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
private:
enum class Phase : std::uint8_t { Rolling,
FadingOut,
Done };
enum class Phase : std::uint8_t { ROLLING,
FADING_OUT,
DONE };
void render();
static void writeTrickIni();
@@ -45,9 +45,9 @@ namespace scenes {
PaletteFade fade_;
FrameAnimator coche_{8, 60, true}; // 8 frames × 60 ms (~3 × 20 ms tick vell)
Phase phase_{Phase::Rolling};
Phase phase_{Phase::ROLLING};
int contador_{1};
int contador_acc_ms_{0};
};
} // namespace scenes
} // namespace Scenes
+2 -2
View File
@@ -2,7 +2,7 @@
#include <algorithm>
namespace scenes {
namespace Scenes {
FrameAnimator::FrameAnimator(int num_frames, int frame_ms, bool loop)
: num_frames_(std::max(1, num_frames)),
@@ -35,4 +35,4 @@ namespace scenes {
finished_ = false;
}
} // namespace scenes
} // namespace Scenes
+2 -2
View File
@@ -1,6 +1,6 @@
#pragma once
namespace scenes {
namespace Scenes {
// Cicla per un conjunt de frames numerats (0..num_frames-1) avançant un
// frame cada `frame_ms` mil·lisegons. No carrega ni dibuixa cap sprite —
@@ -31,4 +31,4 @@ namespace scenes {
bool finished_{false};
};
} // namespace scenes
} // namespace Scenes
+87 -92
View File
@@ -37,11 +37,11 @@ namespace {
} // namespace
namespace scenes {
namespace Scenes {
IntroNewLogoScene::IntroNewLogoScene() = default;
// No alliberem `pal_`: JD8_SetScreenPalette n'ha pres ownership i el
// No alliberem `pal_`: Jd8::setScreenPalette n'ha pres ownership i el
// proper SetScreenPalette / FadeToPal el lliurarà. Alliberar-lo ací
// provocaria double free.
IntroNewLogoScene::~IntroNewLogoScene() = default;
@@ -50,17 +50,17 @@ namespace scenes {
playMusic("music/menu.ogg");
gfx_ = SurfaceHandle("gfx/logo_new.gif");
pal_ = JD8_LoadPalette("gfx/logo_new.gif");
JD8_SetScreenPalette(pal_);
pal_ = Jd8::loadPalette("gfx/logo_new.gif");
Jd8::setScreenPalette(pal_);
// Surface auxiliar omplida amb el color del cursor — permet pintar
// el "subratllat" amb un blit normal.
cursor_surf_.adopt(JD8_NewSurface());
cursor_surf_.adopt(Jd8::newSurface());
std::memset(cursor_surf_.get(), CURSOR_COLOR, 64000);
JD8_ClearScreen(0);
Jd8::clearScreen(0);
phase_ = Phase::Initial;
phase_ = Phase::INITIAL;
phase_acc_ms_ = 0;
reveal_letter_ = 0;
reveal_cursor_visible_ = true;
@@ -69,35 +69,35 @@ namespace scenes {
void IntroNewLogoScene::render() {
switch (phase_) {
case Phase::Initial:
JD8_ClearScreen(0);
case Phase::INITIAL:
Jd8::clearScreen(0);
break;
case Phase::Revealing: {
JD8_ClearScreen(0);
JD8_Blit(LOGO_DST_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[reveal_letter_], LOGO_HEIGHT);
case Phase::REVEALING: {
Jd8::clearScreen(0);
Jd8::blit(LOGO_DST_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[reveal_letter_], LOGO_HEIGHT);
if (reveal_cursor_visible_) {
JD8_Blit(CURSOR_X[reveal_letter_], CURSOR_Y, cursor_surf_, 0, 0, CURSOR_W, CURSOR_H);
Jd8::blit(CURSOR_X[reveal_letter_], CURSOR_Y, cursor_surf_, 0, 0, CURSOR_W, CURSOR_H);
}
break;
}
case Phase::FullLogoFlash:
JD8_ClearScreen(0);
JD8_Blit(LOGO_DST_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[8], LOGO_HEIGHT);
JD8_Blit(CURSOR_X[8], CURSOR_Y, cursor_surf_, 0, 0, CURSOR_W, CURSOR_H);
case Phase::FULL_LOGO_FLASH:
Jd8::clearScreen(0);
Jd8::blit(LOGO_DST_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[8], LOGO_HEIGHT);
Jd8::blit(CURSOR_X[8], CURSOR_Y, cursor_surf_, 0, 0, CURSOR_W, CURSOR_H);
break;
case Phase::PaletteCycle:
case Phase::FinalWait:
case Phase::PALETTE_CYCLE:
case Phase::FINAL_WAIT:
// Logo complet sense cursor — els pixels del cursor
// ciclarien de color durant el cicle de paleta.
JD8_ClearScreen(0);
JD8_Blit(LOGO_DST_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[8], LOGO_HEIGHT);
Jd8::clearScreen(0);
Jd8::blit(LOGO_DST_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[8], LOGO_HEIGHT);
break;
case Phase::Sprites:
case Phase::Done:
case Phase::SPRITES:
case Phase::DONE:
break;
}
}
@@ -132,100 +132,95 @@ namespace scenes {
}
}
void IntroNewLogoScene::tick(int delta_ms) {
// Qualsevol tecla durant el revelat o el ciclo de paleta salta
// TOTA la intro (inclou saltar la fase de sprites). Durant Sprites
// deixem que la sub-escena gestione el seu propi skip (que a més
// respecta la fase "final" no skippable de la variant 0).
if (phase_ != Phase::Sprites && phase_ != Phase::Done && JI_AnyKey()) {
info::ctx.num_piramide = 0;
phase_ = Phase::Done;
void IntroNewLogoScene::advanceRevealing(int delta_ms) {
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ < REVEAL_FRAME_MS) {
return;
}
phase_acc_ms_ = 0;
reveal_cursor_visible_ = !reveal_cursor_visible_;
if (!reveal_cursor_visible_) {
return;
}
++reveal_letter_;
if (reveal_letter_ >= 9) {
phase_ = Phase::FULL_LOGO_FLASH;
reveal_letter_ = 8;
}
}
void IntroNewLogoScene::advancePaletteStep(int delta_ms) {
phase_acc_ms_ += delta_ms;
while (phase_acc_ms_ >= PALETTE_CYCLE_STEP_MS && palette_step_ < PALETTE_CYCLE_STEPS) {
phase_acc_ms_ -= PALETTE_CYCLE_STEP_MS;
advancePaletteCycle();
++palette_step_;
}
render();
if (palette_step_ >= PALETTE_CYCLE_STEPS) {
phase_ = Phase::FINAL_WAIT;
phase_acc_ms_ = 0;
}
}
void IntroNewLogoScene::advanceSpritesPhase(int delta_ms) {
if (!sprites_scene_) {
sprites_scene_ = std::make_unique<IntroSpritesScene>(std::move(gfx_));
sprites_scene_->onEnter();
}
sprites_scene_->tick(delta_ms);
if (sprites_scene_->done()) {
Info::ctx.num_piramide = 0;
phase_ = Phase::DONE;
}
}
void IntroNewLogoScene::tick(int delta_ms) {
// Qualsevol tecla durant el revelat o el ciclo de paleta salta
// TOTA la intro (inclou saltar la fase de sprites). Durant SPRITES
// deixem que la sub-escena gestione el seu propi skip.
if (phase_ != Phase::SPRITES && phase_ != Phase::DONE && Ji::anyKey()) {
Info::ctx.num_piramide = 0;
phase_ = Phase::DONE;
return;
}
switch (phase_) {
case Phase::Initial:
case Phase::INITIAL:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= INITIAL_MS) {
phase_ = Phase::Revealing;
phase_ = Phase::REVEALING;
phase_acc_ms_ = 0;
}
break;
case Phase::Revealing:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= REVEAL_FRAME_MS) {
phase_acc_ms_ = 0;
reveal_cursor_visible_ = !reveal_cursor_visible_;
// Quan acabem els dos frames d'una lletra (cursor on → off),
// passem a la següent lletra.
if (reveal_cursor_visible_) {
++reveal_letter_;
if (reveal_letter_ >= 9) {
phase_ = Phase::FullLogoFlash;
reveal_letter_ = 8;
}
}
}
case Phase::REVEALING:
advanceRevealing(delta_ms);
break;
case Phase::FullLogoFlash:
case Phase::FULL_LOGO_FLASH:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= FULL_LOGO_MS) {
phase_ = Phase::PaletteCycle;
phase_ = Phase::PALETTE_CYCLE;
phase_acc_ms_ = 0;
}
break;
case Phase::PaletteCycle:
phase_acc_ms_ += delta_ms;
// Avancem passos de paleta cada 20 ms. Si el delta és gran,
// consumim múltiples passos en la mateixa crida.
while (phase_acc_ms_ >= PALETTE_CYCLE_STEP_MS &&
palette_step_ < PALETTE_CYCLE_STEPS) {
phase_acc_ms_ -= PALETTE_CYCLE_STEP_MS;
advancePaletteCycle();
++palette_step_;
}
render();
if (palette_step_ >= PALETTE_CYCLE_STEPS) {
phase_ = Phase::FinalWait;
phase_acc_ms_ = 0;
}
case Phase::PALETTE_CYCLE:
advancePaletteStep(delta_ms);
break;
case Phase::FinalWait:
case Phase::FINAL_WAIT:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= FINAL_WAIT_MS) {
phase_ = Phase::Sprites;
phase_ = Phase::SPRITES;
}
break;
case Phase::Sprites:
// Sub-escena construïda al primer tick. Transferim el gfx_
// per move — la sub-escena se n'ocupa fins que es destruix.
// Cada tick successiu delega l'animació dels sprites.
if (!sprites_scene_) {
sprites_scene_ = std::make_unique<IntroSpritesScene>(std::move(gfx_));
sprites_scene_->onEnter();
}
sprites_scene_->tick(delta_ms);
if (sprites_scene_->done()) {
// El vell `Go()` post-switch feia `num_piramide = 0`
// per passar al menú. Sense açò el while del fiber
// tornaria a crear IntroNewLogoScene infinitament.
info::ctx.num_piramide = 0;
phase_ = Phase::Done;
}
case Phase::SPRITES:
advanceSpritesPhase(delta_ms);
break;
case Phase::Done:
case Phase::DONE:
break;
}
}
} // namespace scenes
} // namespace Scenes
+16 -12
View File
@@ -8,7 +8,7 @@
#include "game/scenes/scene.hpp"
#include "game/scenes/surface_handle.hpp"
namespace scenes {
namespace Scenes {
// Intro "moderna" del logo Jailgames amb revelat lletra-a-lletra +
// ciclo de paleta final. Reemplaça `ModuleSequence::doIntroNewLogo()`.
@@ -37,32 +37,36 @@ namespace scenes {
void onEnter() override;
void tick(int delta_ms) override;
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::Done; }
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
private:
enum class Phase : std::uint8_t {
Initial, // pantalla negra 1000 ms
Revealing, // 9 × 2 frames × 150 ms cada un
FullLogoFlash, // logo complet + cursor, 200 ms
PaletteCycle, // 256 passos × 20 ms modificant paleta
FinalWait, // 20 ms final
Sprites, // tick delegat a IntroSpritesScene fins que acaba
Done,
INITIAL, // pantalla negra 1000 ms
REVEALING, // 9 × 2 frames × 150 ms cada un
FULL_LOGO_FLASH, // logo complet + cursor, 200 ms
PALETTE_CYCLE, // 256 passos × 20 ms modificant paleta
FINAL_WAIT, // 20 ms final
SPRITES, // tick delegat a IntroSpritesScene fins que acaba
DONE,
};
void render();
void advancePaletteCycle();
// Helpers per a `tick()` — extrets per reduir complexitat cognitiva.
void advanceRevealing(int delta_ms);
void advancePaletteStep(int delta_ms);
void advanceSpritesPhase(int delta_ms);
SurfaceHandle gfx_;
SurfaceHandle cursor_surf_;
JD8_Palette pal_{nullptr}; // propietat transferida a main_palette via SetScreenPalette
Jd8::Palette pal_{nullptr}; // propietat transferida a main_palette via SetScreenPalette
std::unique_ptr<IntroSpritesScene> sprites_scene_;
Phase phase_{Phase::Initial};
Phase phase_{Phase::INITIAL};
int phase_acc_ms_{0};
int reveal_letter_{0};
bool reveal_cursor_visible_{true};
int palette_step_{0};
};
} // namespace scenes
} // namespace Scenes
+36 -36
View File
@@ -7,7 +7,7 @@
namespace {
// Timings idèntics als del vell `doIntro()`: el JG_SetUpdateTicks(1000)
// Timings idèntics als del vell `doIntro()`: el Jg::setUpdateTicks(1000)
// inicial, (100) per a les 3 primeres lletres (J, A, I), (200) per a
// "JAIL" i el seu clear, (100) per a les 4 lletres centrals
// (G, A, M, E) i (200) per a la resta fins al cicle de paleta.
@@ -50,16 +50,16 @@ namespace {
// IntroScene només s'activa quan use_new_logo == false, així que la
// branca use_new_logo d'aquell helper aquí no es necessita.
void drawWordmark(const Uint8* gfx) {
JD8_Blit(43, 78, gfx, 43, 155, 231, 45);
Jd8::blit(43, 78, gfx, 43, 155, 231, 45);
}
} // namespace
namespace scenes {
namespace Scenes {
IntroScene::IntroScene() = default;
// No alliberem `pal_`: JD8_SetScreenPalette n'ha pres ownership i el
// No alliberem `pal_`: Jd8::setScreenPalette n'ha pres ownership i el
// proper SetScreenPalette / FadeToPal la lliurarà. Alliberar-la ací
// provocaria double free.
IntroScene::~IntroScene() = default;
@@ -68,12 +68,12 @@ namespace scenes {
playMusic("music/menu.ogg");
gfx_ = SurfaceHandle("gfx/logo.gif");
pal_ = JD8_LoadPalette("gfx/logo.gif");
JD8_SetScreenPalette(pal_);
pal_ = Jd8::loadPalette("gfx/logo.gif");
Jd8::setScreenPalette(pal_);
JD8_ClearScreen(0);
Jd8::clearScreen(0);
phase_ = Phase::InitialWait;
phase_ = Phase::INITIAL_WAIT;
phase_acc_ms_ = 0;
reveal_index_ = 0;
palette_step_ = 0;
@@ -81,37 +81,37 @@ namespace scenes {
void IntroScene::render() {
switch (phase_) {
case Phase::InitialWait:
JD8_ClearScreen(0);
case Phase::INITIAL_WAIT:
Jd8::clearScreen(0);
break;
case Phase::Reveal: {
case Phase::REVEAL: {
const RevealStep& s = REVEAL_STEPS[reveal_index_];
if (s.clear) {
JD8_ClearScreen(0);
Jd8::clearScreen(0);
}
if (s.wordmark) {
drawWordmark(gfx_);
} else if (s.body_w > 0) {
JD8_Blit(43, 78, gfx_, 43, 155, s.body_w, 45);
Jd8::blit(43, 78, gfx_, 43, 155, s.body_w, 45);
}
if (s.plane_x >= 0) {
JD8_Blit(s.plane_x, 78, gfx_, 274, 155, 27, 45);
Jd8::blit(s.plane_x, 78, gfx_, 274, 155, 27, 45);
}
break;
}
case Phase::PaletteCycle:
case Phase::FinalWait:
case Phase::PALETTE_CYCLE:
case Phase::FINAL_WAIT:
// Wordmark complet fix mentre cicla la paleta — l'últim
// pas del revelat (PAS 15) deixa la pantalla en aquest mateix
// estat, i el vell doIntro no redibuixava durant el cicle.
JD8_ClearScreen(0);
Jd8::clearScreen(0);
drawWordmark(gfx_);
break;
case Phase::Sprites:
case Phase::Done:
case Phase::SPRITES:
case Phase::DONE:
break;
}
}
@@ -149,39 +149,39 @@ namespace scenes {
void IntroScene::tick(int delta_ms) {
// Qualsevol tecla durant revelat/paleta salta TOTA la intro
// (inclou saltar la fase de sprites). Durant Sprites deixem que
// (inclou saltar la fase de sprites). Durant SPRITES deixem que
// la sub-escena gestione el seu propi skip internament, que a més
// respecta la fase "final" no skippable de la variant 0.
if (phase_ != Phase::Sprites && phase_ != Phase::Done && JI_AnyKey()) {
info::ctx.num_piramide = 0;
phase_ = Phase::Done;
if (phase_ != Phase::SPRITES && phase_ != Phase::DONE && Ji::anyKey()) {
Info::ctx.num_piramide = 0;
phase_ = Phase::DONE;
return;
}
switch (phase_) {
case Phase::InitialWait:
case Phase::INITIAL_WAIT:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= INITIAL_MS) {
phase_ = Phase::Reveal;
phase_ = Phase::REVEAL;
phase_acc_ms_ = 0;
reveal_index_ = 0;
}
break;
case Phase::Reveal:
case Phase::REVEAL:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= REVEAL_STEPS[reveal_index_].duration_ms) {
phase_acc_ms_ = 0;
++reveal_index_;
if (reveal_index_ >= REVEAL_COUNT) {
phase_ = Phase::PaletteCycle;
phase_ = Phase::PALETTE_CYCLE;
}
}
break;
case Phase::PaletteCycle:
case Phase::PALETTE_CYCLE:
phase_acc_ms_ += delta_ms;
// Avancem tants passos com permet el delta, per evitar
// saltar-ne si el frame ha vingut lent.
@@ -193,20 +193,20 @@ namespace scenes {
}
render();
if (palette_step_ >= PALETTE_CYCLE_STEPS) {
phase_ = Phase::FinalWait;
phase_ = Phase::FINAL_WAIT;
phase_acc_ms_ = 0;
}
break;
case Phase::FinalWait:
case Phase::FINAL_WAIT:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= FINAL_WAIT_MS) {
phase_ = Phase::Sprites;
phase_ = Phase::SPRITES;
}
break;
case Phase::Sprites:
case Phase::SPRITES:
// Sub-escena construïda al vol al primer tick d'aquesta fase.
// Transferim el gfx_ per move — la sub-escena se n'ocupa
// fins que es destruix. Una vegada feta, els ticks delegats
@@ -220,14 +220,14 @@ namespace scenes {
// Equivalent al vell `Go()` post-switch: passem al menú.
// Sense açò el while del fiber tornaria a crear IntroScene
// infinitament amb num_piramide encara a 255.
info::ctx.num_piramide = 0;
phase_ = Phase::Done;
Info::ctx.num_piramide = 0;
phase_ = Phase::DONE;
}
break;
case Phase::Done:
case Phase::DONE:
break;
}
}
} // namespace scenes
} // namespace Scenes
+11 -11
View File
@@ -8,7 +8,7 @@
#include "game/scenes/scene.hpp"
#include "game/scenes/surface_handle.hpp"
namespace scenes {
namespace Scenes {
// Intro "legacy" del wordmark JAILGAMES lletra a lletra + cicle de paleta.
// Reemplaça `ModuleSequence::doIntro()`. S'activa quan
@@ -38,29 +38,29 @@ namespace scenes {
void onEnter() override;
void tick(int delta_ms) override;
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::Done; }
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
private:
enum class Phase : std::uint8_t {
InitialWait, // 1000 ms pantalla negra
Reveal, // 15 passos del wordmark
PaletteCycle, // 256 × 20 ms mutant pal[16..31]
FinalWait, // 200 ms abans de la sub-escena de sprites
Sprites, // tick delegat a IntroSpritesScene fins que acaba
Done,
INITIAL_WAIT, // 1000 ms pantalla negra
REVEAL, // 15 passos del wordmark
PALETTE_CYCLE, // 256 × 20 ms mutant pal[16..31]
FINAL_WAIT, // 200 ms abans de la sub-escena de sprites
SPRITES, // tick delegat a IntroSpritesScene fins que acaba
DONE,
};
void render();
void advancePaletteCycle();
SurfaceHandle gfx_;
JD8_Palette pal_{nullptr}; // propietat transferida a main_palette via SetScreenPalette
Jd8::Palette pal_{nullptr}; // propietat transferida a main_palette via SetScreenPalette
std::unique_ptr<IntroSpritesScene> sprites_scene_;
Phase phase_{Phase::InitialWait};
Phase phase_{Phase::INITIAL_WAIT};
int phase_acc_ms_{0};
int reveal_index_{0};
int palette_step_{0};
};
} // namespace scenes
} // namespace Scenes
+153 -153
View File
@@ -9,7 +9,7 @@
namespace {
// Duració d'un pas. El vell doIntroSprites feia JG_SetUpdateTicks(20);
// Duració d'un pas. El vell doIntroSprites feia Jg::setUpdateTicks(20);
// cada iteració del seu for (i) consumia un tick de 20 ms.
constexpr int TICK_MS = 20;
@@ -18,13 +18,13 @@ namespace {
// Cada sprite ocupa 15×15 px, disposats horitzontalment per fila.
// Els valors són els offsets x (la y la posa l'invocador al src_y).
// Derivats dels `fr_ani_N[i] = ...` del vell doIntroSprites.
constexpr Uint16 fr1[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180}; // camina dreta (y=0)
constexpr Uint16 fr2[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180}; // camina esquerra (y=15)
constexpr Uint16 fr3[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150}; // trau mapa dreta (y=30)
constexpr Uint16 fr4[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150}; // trau mapa esquerra (y=45)
constexpr Uint16 fr5[] = {165, 180, 195, 210, 225, 240, 255, 270, 285, 300, 300, 285, 270, 255, 240, 225, 210, 195, 180, 165}; // bot de susto (y=45, mirror)
constexpr Uint16 fr6[] = {0, 15, 30, 45, 60, 75, 90, 105}; // momia (y=60)
constexpr Uint16 fr7[] = {75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 255, 270, // paper (y=75, idx 0..13)
constexpr Uint16 FR1[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180}; // camina dreta (y=0)
constexpr Uint16 FR2[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180}; // camina esquerra (y=15)
constexpr Uint16 FR3[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150}; // trau mapa dreta (y=30)
constexpr Uint16 FR4[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150}; // trau mapa esquerra (y=45)
constexpr Uint16 FR5[] = {165, 180, 195, 210, 225, 240, 255, 270, 285, 300, 300, 285, 270, 255, 240, 225, 210, 195, 180, 165}; // bot de susto (y=45, mirror)
constexpr Uint16 FR6[] = {0, 15, 30, 45, 60, 75, 90, 105}; // momia (y=60)
constexpr Uint16 FR7[] = {75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 255, 270, // paper (y=75, idx 0..13)
0,
15,
30,
@@ -40,10 +40,10 @@ namespace {
180,
195,
210}; // sombra (y=105, idx 14..28)
constexpr Uint16 fr8[] = {15, 30, 45, 60}; // pedra (y=75)
constexpr Uint16 fr9[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225}; // prota ball (y=120)
constexpr Uint16 fr10[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225}; // momia ball (y=135)
constexpr Uint16 fr11[] = {15, 30, 45, 60, 75, 60}; // altaveu (y=90, [5]=[3] pel loop de 4)
constexpr Uint16 FR8[] = {15, 30, 45, 60}; // pedra (y=75)
constexpr Uint16 FR9[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225}; // prota ball (y=120)
constexpr Uint16 FR10[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225}; // momia ball (y=135)
constexpr Uint16 FR11[] = {15, 30, 45, 60, 75, 60}; // altaveu (y=90, [5]=[3] pel loop de 4)
constexpr Uint16 CREU = 75; // src_y de la creu (overlay)
constexpr Uint16 INTERROGANT = 90; // src_y del signe d'interrogant
@@ -55,9 +55,9 @@ namespace {
void drawWordmark(const Uint8* gfx) {
if (Options::game.use_new_logo) {
// Centrat: (320 188) / 2 = 66 (IntroNewLogoScene usa la mateixa x).
JD8_Blit(66, 78, gfx, 60, 158, 188, 28);
Jd8::blit(66, 78, gfx, 60, 158, 188, 28);
} else {
JD8_Blit(43, 78, gfx, 43, 155, 231, 45);
Jd8::blit(43, 78, gfx, 43, 155, 231, 45);
}
}
@@ -78,243 +78,243 @@ namespace {
// Variant 0 — Interrogant / Momia
// =========================================================================
void v0_walk_right(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v0WalkRight(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(i, 150, gfx, fr1[(i / 5) % 13], 0, 15, 15, 0);
Jd8::blitCK(i, 150, gfx, FR1[(i / 5) % 13], 0, 15, 15, 0);
}
void v0_pull_map_right(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v0PullMapRight(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 150, gfx, fr3[std::min(i / 5, 10)], 30, 15, 15, 0);
Jd8::blitCK(200, 150, gfx, FR3[std::min(i / 5, 10)], 30, 15, 15, 0);
}
void v0_walk_left_to_80(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v0WalkLeftTo80(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(i, 150, gfx, fr2[(i / 5) % 13], 15, 15, 15, 0);
Jd8::blitCK(i, 150, gfx, FR2[(i / 5) % 13], 15, 15, 15, 0);
}
void v0_pull_map_left(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v0PullMapLeft(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(80, 150, gfx, fr4[std::min(i / 5, 10)], 45, 15, 15, 0);
Jd8::blitCK(80, 150, gfx, FR4[std::min(i / 5, 10)], 45, 15, 15, 0);
}
void v0_momia_left(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v0MomiaLeft(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(i, 150, gfx, fr6[(i / 5) % 8], 60, 15, 15, 0);
JD8_BlitCK(80, 150, gfx, fr4[10], 45, 15, 15, 0);
Jd8::blitCK(i, 150, gfx, FR6[(i / 5) % 8], 60, 15, 15, 0);
Jd8::blitCK(80, 150, gfx, FR4[10], 45, 15, 15, 0);
}
void v0_turn(const Uint8* gfx, int /*i*/) {
JD8_ClearScreen(0);
void v0Turn(const Uint8* gfx, int /*i*/) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(80, 150, gfx, fr1[1], 0, 15, 15, 0);
JD8_BlitCK(95, 150, gfx, fr6[4], 60, 15, 15, 0);
JD8_BlitCK(80, 133, gfx, 0, INTERROGANT, 15, 15, 0);
Jd8::blitCK(80, 150, gfx, FR1[1], 0, 15, 15, 0);
Jd8::blitCK(95, 150, gfx, FR6[4], 60, 15, 15, 0);
Jd8::blitCK(80, 133, gfx, 0, INTERROGANT, 15, 15, 0);
}
void v0_jump1(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v0Jump1(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(80, 150 - ((i % 50) / 5), gfx, fr5[std::min(i / 5, 19)], 45, 15, 15, 0);
JD8_BlitCK(95, 150, gfx, fr6[4], 60, 15, 15, 0);
Jd8::blitCK(80, 150 - ((i % 50) / 5), gfx, FR5[std::min(i / 5, 19)], 45, 15, 15, 0);
Jd8::blitCK(95, 150, gfx, FR6[4], 60, 15, 15, 0);
}
void v0_jump2(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v0Jump2(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(80, 140 + ((i % 50) / 5), gfx, fr5[std::min(i / 5, 19)], 45, 15, 15, 0);
JD8_BlitCK(95, 150, gfx, fr6[4], 60, 15, 15, 0);
Jd8::blitCK(80, 140 + ((i % 50) / 5), gfx, FR5[std::min(i / 5, 19)], 45, 15, 15, 0);
Jd8::blitCK(95, 150, gfx, FR6[4], 60, 15, 15, 0);
}
void v0_walk_final(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v0WalkFinal(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(i, 150, gfx, fr2[(i / 5) % 13], 15, 15, 15, 0);
JD8_BlitCK(95, 150, gfx, fr6[4], 60, 15, 15, 0);
Jd8::blitCK(i, 150, gfx, FR2[(i / 5) % 13], 15, 15, 15, 0);
Jd8::blitCK(95, 150, gfx, FR6[4], 60, 15, 15, 0);
}
void v0_final(const Uint8* gfx, int /*i*/) {
JD8_ClearScreen(0);
void v0Final(const Uint8* gfx, int /*i*/) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(95, 150, gfx, fr6[4], 60, 15, 15, 0);
JD8_BlitCK(95, 133, gfx, 0, INTERROGANT, 15, 15, 0);
Jd8::blitCK(95, 150, gfx, FR6[4], 60, 15, 15, 0);
Jd8::blitCK(95, 133, gfx, 0, INTERROGANT, 15, 15, 0);
}
constexpr SpritePhase variant_0[] = {
{.start_i = 0, .end_i = 200, .render = v0_walk_right, .skippable = true},
{.start_i = 0, .end_i = 200, .render = v0_pull_map_right, .skippable = true},
{.start_i = 200, .end_i = 0, .render = v0_pull_map_right, .skippable = true}, // guarda el mapa (reprodueix inversament)
{.start_i = 200, .end_i = 80, .render = v0_walk_left_to_80, .skippable = true},
{.start_i = 0, .end_i = 200, .render = v0_pull_map_left, .skippable = true},
{.start_i = 300, .end_i = 95, .render = v0_momia_left, .skippable = true},
{.start_i = 0, .end_i = 50, .render = v0_turn, .skippable = true},
{.start_i = 0, .end_i = 49, .render = v0_jump1, .skippable = true},
{.start_i = 50, .end_i = 99, .render = v0_jump2, .skippable = true},
{.start_i = 80, .end_i = 0, .render = v0_walk_final, .skippable = true},
{.start_i = 0, .end_i = 150, .render = v0_final, .skippable = true},
constexpr SpritePhase VARIANT_0[] = {
{.start_i = 0, .end_i = 200, .render = v0WalkRight, .skippable = true},
{.start_i = 0, .end_i = 200, .render = v0PullMapRight, .skippable = true},
{.start_i = 200, .end_i = 0, .render = v0PullMapRight, .skippable = true}, // guarda el mapa (reprodueix inversament)
{.start_i = 200, .end_i = 80, .render = v0WalkLeftTo80, .skippable = true},
{.start_i = 0, .end_i = 200, .render = v0PullMapLeft, .skippable = true},
{.start_i = 300, .end_i = 95, .render = v0MomiaLeft, .skippable = true},
{.start_i = 0, .end_i = 50, .render = v0Turn, .skippable = true},
{.start_i = 0, .end_i = 49, .render = v0Jump1, .skippable = true},
{.start_i = 50, .end_i = 99, .render = v0Jump2, .skippable = true},
{.start_i = 80, .end_i = 0, .render = v0WalkFinal, .skippable = true},
{.start_i = 0, .end_i = 150, .render = v0Final, .skippable = true},
};
// =========================================================================
// Variant 1 — Creu / Pedra
// =========================================================================
void v1_walk_right(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v1WalkRight(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
JD8_BlitCK(i, 150, gfx, fr1[(i / 5) % 13], 0, 15, 15, 255);
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
Jd8::blitCK(i, 150, gfx, FR1[(i / 5) % 13], 0, 15, 15, 255);
}
void v1_pull_map(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v1PullMap(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
JD8_BlitCK(200, 150, gfx, fr3[std::min(i / 5, 10)], 30, 15, 15, 255);
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
Jd8::blitCK(200, 150, gfx, FR3[std::min(i / 5, 10)], 30, 15, 15, 255);
}
void v1_interrogant(const Uint8* gfx, int /*i*/) {
JD8_ClearScreen(0);
void v1Interrogant(const Uint8* gfx, int /*i*/) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
JD8_BlitCK(200, 134, gfx, 0, INTERROGANT, 15, 15, 255);
JD8_BlitCK(200, 150, gfx, fr3[10], 30, 15, 15, 255);
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
Jd8::blitCK(200, 134, gfx, 0, INTERROGANT, 15, 15, 255);
Jd8::blitCK(200, 150, gfx, FR3[10], 30, 15, 15, 255);
}
void v1_drop_map(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v1DropMap(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
const int idx = std::min(i / 5, 28);
// fr7 té 29 frames dividits en dos grups: paper (idx 0..13, src_y=75)
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
const int IDX = std::min(i / 5, 28);
// FR7 té 29 frames dividits en dos grups: paper (idx 0..13, src_y=75)
// i sombra (idx 14..28, src_y=105). El vell feia una branca al bucle.
if (idx <= 13) {
JD8_BlitCK(200, 150, gfx, fr7[idx], 75, 15, 15, 255);
if (IDX <= 13) {
Jd8::blitCK(200, 150, gfx, FR7[IDX], 75, 15, 15, 255);
} else {
JD8_BlitCK(200, 150, gfx, fr7[idx], 105, 15, 15, 255);
Jd8::blitCK(200, 150, gfx, FR7[IDX], 105, 15, 15, 255);
}
}
void v1_stone_fall(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v1StoneFall(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
JD8_BlitCK(200, 150, gfx, fr7[28], 105, 15, 15, 255);
JD8_BlitCK(200, i * 2, gfx, fr8[0], 75, 15, 15, 255);
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
Jd8::blitCK(200, 150, gfx, FR7[28], 105, 15, 15, 255);
Jd8::blitCK(200, i * 2, gfx, FR8[0], 75, 15, 15, 255);
}
void v1_stone_break(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v1StoneBreak(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
JD8_BlitCK(200, 150, gfx, fr8[i / 10], 75, 15, 15, 255);
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
Jd8::blitCK(200, 150, gfx, FR8[i / 10], 75, 15, 15, 255);
}
void v1_final(const Uint8* gfx, int /*i*/) {
JD8_ClearScreen(0);
void v1Final(const Uint8* gfx, int /*i*/) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
JD8_BlitCK(200, 150, gfx, fr8[1], 75, 15, 15, 255);
JD8_BlitCK(185, 150, gfx, fr8[2], 75, 15, 15, 255);
JD8_BlitCK(215, 150, gfx, fr8[3], 75, 15, 15, 255);
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
Jd8::blitCK(200, 150, gfx, FR8[1], 75, 15, 15, 255);
Jd8::blitCK(185, 150, gfx, FR8[2], 75, 15, 15, 255);
Jd8::blitCK(215, 150, gfx, FR8[3], 75, 15, 15, 255);
}
constexpr SpritePhase variant_1[] = {
{.start_i = 0, .end_i = 200, .render = v1_walk_right, .skippable = true},
{.start_i = 0, .end_i = 300, .render = v1_pull_map, .skippable = true},
{.start_i = 0, .end_i = 100, .render = v1_interrogant, .skippable = true},
{.start_i = 0, .end_i = 200, .render = v1_drop_map, .skippable = true},
{.start_i = 0, .end_i = 75, .render = v1_stone_fall, .skippable = true},
{.start_i = 0, .end_i = 19, .render = v1_stone_break, .skippable = true},
{.start_i = 0, .end_i = 200, .render = v1_final, .skippable = true},
constexpr SpritePhase VARIANT_1[] = {
{.start_i = 0, .end_i = 200, .render = v1WalkRight, .skippable = true},
{.start_i = 0, .end_i = 300, .render = v1PullMap, .skippable = true},
{.start_i = 0, .end_i = 100, .render = v1Interrogant, .skippable = true},
{.start_i = 0, .end_i = 200, .render = v1DropMap, .skippable = true},
{.start_i = 0, .end_i = 75, .render = v1StoneFall, .skippable = true},
{.start_i = 0, .end_i = 19, .render = v1StoneBreak, .skippable = true},
{.start_i = 0, .end_i = 200, .render = v1Final, .skippable = true},
};
// =========================================================================
// Variant 2 — Ball de carnaval
// =========================================================================
void v2_approach(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v2Approach(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(i, 150, gfx, fr1[(i / 5) % 13], 0, 15, 15, 255);
JD8_BlitCK(304 - i, 150, gfx, fr6[(i / 10) % 8], 60, 15, 15, 255);
Jd8::blitCK(i, 150, gfx, FR1[(i / 5) % 13], 0, 15, 15, 255);
Jd8::blitCK(304 - i, 150, gfx, FR6[(i / 10) % 8], 60, 15, 15, 255);
}
void v2_still(const Uint8* gfx, int /*i*/) {
JD8_ClearScreen(0);
void v2Still(const Uint8* gfx, int /*i*/) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(145, 150, gfx, fr1[1], 0, 15, 15, 255);
JD8_BlitCK(160, 150, gfx, fr6[1], 60, 15, 15, 255);
Jd8::blitCK(145, 150, gfx, FR1[1], 0, 15, 15, 255);
Jd8::blitCK(160, 150, gfx, FR6[1], 60, 15, 15, 255);
}
void v2_horn(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v2Horn(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(125, 150, gfx, fr11[(i / 10) % 2], 90, 15, 15, 255);
JD8_BlitCK(145, 150, gfx, fr1[1], 0, 15, 15, 255);
JD8_BlitCK(160, 150, gfx, fr6[1], 60, 15, 15, 255);
Jd8::blitCK(125, 150, gfx, FR11[(i / 10) % 2], 90, 15, 15, 255);
Jd8::blitCK(145, 150, gfx, FR1[1], 0, 15, 15, 255);
Jd8::blitCK(160, 150, gfx, FR6[1], 60, 15, 15, 255);
}
void v2_ball(const Uint8* gfx, int i) {
JD8_ClearScreen(0);
void v2Ball(const Uint8* gfx, int i) {
Jd8::clearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(145, 150, gfx, fr9[(i / 10) % 16], 120, 15, 15, 255);
JD8_BlitCK(160, 150, gfx, fr10[(i / 10) % 16], 135, 15, 15, 255);
JD8_BlitCK(125, 150, gfx, fr11[((i / 5) % 4) + 2], 90, 15, 15, 255);
Jd8::blitCK(145, 150, gfx, FR9[(i / 10) % 16], 120, 15, 15, 255);
Jd8::blitCK(160, 150, gfx, FR10[(i / 10) % 16], 135, 15, 15, 255);
Jd8::blitCK(125, 150, gfx, FR11[((i / 5) % 4) + 2], 90, 15, 15, 255);
}
constexpr SpritePhase variant_2[] = {
{.start_i = 0, .end_i = 145, .render = v2_approach, .skippable = true},
{.start_i = 0, .end_i = 100, .render = v2_still, .skippable = true},
{.start_i = 0, .end_i = 50, .render = v2_horn, .skippable = true},
{.start_i = 0, .end_i = 800, .render = v2_ball, .skippable = true},
constexpr SpritePhase VARIANT_2[] = {
{.start_i = 0, .end_i = 145, .render = v2Approach, .skippable = true},
{.start_i = 0, .end_i = 100, .render = v2Still, .skippable = true},
{.start_i = 0, .end_i = 50, .render = v2Horn, .skippable = true},
{.start_i = 0, .end_i = 800, .render = v2Ball, .skippable = true},
};
// =========================================================================
// Dispatch per variant
// =========================================================================
auto variant_table(int variant) -> const SpritePhase* {
auto variantTable(int variant) -> const SpritePhase* {
switch (variant) {
case 0:
return variant_0;
return VARIANT_0;
case 1:
return variant_1;
return VARIANT_1;
case 2:
return variant_2;
return VARIANT_2;
default:
return variant_0;
return VARIANT_0;
}
}
auto variant_length(int variant) -> int {
auto variantLength(int variant) -> int {
switch (variant) {
case 0:
return sizeof(variant_0) / sizeof(variant_0[0]);
return sizeof(VARIANT_0) / sizeof(VARIANT_0[0]);
case 1:
return sizeof(variant_1) / sizeof(variant_1[0]);
return sizeof(VARIANT_1) / sizeof(VARIANT_1[0]);
case 2:
return sizeof(variant_2) / sizeof(variant_2[0]);
return sizeof(VARIANT_2) / sizeof(VARIANT_2[0]);
default:
return 0;
}
}
auto phase_step_count(const SpritePhase& p) -> int {
auto phaseStepCount(const SpritePhase& p) -> int {
return std::abs(p.end_i - p.start_i) + 1;
}
auto phase_current_i(const SpritePhase& p, int step) -> int {
auto phaseCurrentI(const SpritePhase& p, int step) -> int {
return p.end_i >= p.start_i ? p.start_i + step : p.start_i - step;
}
} // namespace
namespace scenes {
namespace Scenes {
IntroSpritesScene::IntroSpritesScene(SurfaceHandle&& gfx)
: gfx_(std::move(gfx)) {}
@@ -330,9 +330,9 @@ namespace scenes {
done_ = false;
// Renderitzem ja el primer frame (step 0 de la primera fase) perquè
// el JD8_Flip del mini-loop del fiber el pinte al primer cicle.
const SpritePhase* phases = variant_table(variant_);
phases[0].render(gfx_.get(), phase_current_i(phases[0], 0));
// el Jd8::flip del mini-loop del fiber el pinte al primer cicle.
const SpritePhase* phases = variantTable(variant_);
phases[0].render(gfx_.get(), phaseCurrentI(phases[0], 0));
}
void IntroSpritesScene::tick(int delta_ms) {
@@ -340,13 +340,13 @@ namespace scenes {
return;
}
const SpritePhase* phases = variant_table(variant_);
const int num_phases = variant_length(variant_);
const SpritePhase* phases = variantTable(variant_);
const int NUM_PHASES = variantLength(variant_);
// Skip per tecla. Durant la fase marcada com a no skippable (només
// v0_final al vell codi) s'ignora — preserva la semàntica del vell
// v0Final al vell codi) s'ignora — preserva la semàntica del vell
// bucle final de la variant 0 que no cridava wait_frame_or_skip.
if (phases[phase_].skippable && JI_AnyKey()) {
if (phases[phase_].skippable && Ji::anyKey()) {
done_ = true;
return;
}
@@ -355,16 +355,16 @@ namespace scenes {
while (step_acc_ms_ >= TICK_MS && !done_) {
step_acc_ms_ -= TICK_MS;
++phase_step_;
if (phase_step_ >= phase_step_count(phases[phase_])) {
if (phase_step_ >= phaseStepCount(phases[phase_])) {
++phase_;
phase_step_ = 0;
if (phase_ >= num_phases) {
if (phase_ >= NUM_PHASES) {
done_ = true;
return;
}
}
}
phases[phase_].render(gfx_.get(), phase_current_i(phases[phase_], phase_step_));
phases[phase_].render(gfx_.get(), phaseCurrentI(phases[phase_], phase_step_));
}
} // namespace scenes
} // namespace Scenes
+2 -2
View File
@@ -3,7 +3,7 @@
#include "game/scenes/scene.hpp"
#include "game/scenes/surface_handle.hpp"
namespace scenes {
namespace Scenes {
// Sub-escena de sprites de la intro (prota + momia + mapa + etc).
// Reemplaça `ModuleSequence::doIntroSprites()`. No es registra al
@@ -39,4 +39,4 @@ namespace scenes {
bool done_{false};
};
} // namespace scenes
} // namespace Scenes
+35 -35
View File
@@ -6,68 +6,68 @@
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
namespace scenes {
namespace Scenes {
void MenuScene::onEnter() {
fondo_ = SurfaceHandle("gfx/menu.gif");
gfx_ = SurfaceHandle("gfx/menu2.gif");
// Pintat inicial (congelat durant el fade-in de paleta). El loop
// d'animació repintarà tot des de zero en el primer tick de Showing.
JD8_Blit(fondo_);
JD8_BlitCK(100, 25, gfx_, 0, 74, 124, 68, 255); // logo
JD8_BlitCK(130, 100, gfx_, 0, 0, 80, 74, 255); // camell (frame 0)
JD8_BlitCK(0, 150, gfx_, 0, 150, 320, 50, 255); // base "jdes"
// d'animació repintarà tot des de zero en el primer tick de SHOWING.
Jd8::blit(fondo_);
Jd8::blitCK(100, 25, gfx_, 0, 74, 124, 68, 255); // logo
Jd8::blitCK(130, 100, gfx_, 0, 0, 80, 74, 255); // camell (frame 0)
Jd8::blitCK(0, 150, gfx_, 0, 150, 320, 50, 255); // base "jdes"
JD8_Palette pal = JD8_LoadPalette("gfx/menu2.gif");
Jd8::Palette pal = Jd8::loadPalette("gfx/menu2.gif");
fade_.startFadeTo(pal);
delete[] pal;
phase_ = Phase::FadingIn;
phase_ = Phase::FADING_IN;
}
void MenuScene::render() {
// Cel estàtic (els primers 100 pixels verticals)
JD8_Blit(0, 0, fondo_, 0, 0, 320, 100);
Jd8::blit(0, 0, fondo_, 0, 0, 320, 100);
// Fondo mòvil (horitzó) amb wrap a 320
JD8_BlitCK(horitzo_, 100, fondo_, 0, 100, 320 - horitzo_, 100, 255);
JD8_BlitCK(0, 100, fondo_, 320 - horitzo_, 100, horitzo_, 100, 255);
Jd8::blitCK(horitzo_, 100, fondo_, 0, 100, 320 - horitzo_, 100, 255);
Jd8::blitCK(0, 100, fondo_, 320 - horitzo_, 100, horitzo_, 100, 255);
// Logo i camell animat
JD8_BlitCK(100, 25, gfx_, 0, 74, 124, 68, 255);
JD8_BlitCK(130, 100, gfx_, camello_.frame() * 80, 0, 80, 74, 255);
Jd8::blitCK(100, 25, gfx_, 0, 74, 124, 68, 255);
Jd8::blitCK(130, 100, gfx_, camello_.frame() * 80, 0, 80, 74, 255);
// Palmeres mòvils amb wrap a 320
JD8_BlitCK(palmeres_, 150, gfx_, 0, 150, 320 - palmeres_, 50, 255);
JD8_BlitCK(0, 150, gfx_, 320 - palmeres_, 150, palmeres_, 50, 255);
Jd8::blitCK(palmeres_, 150, gfx_, 0, 150, 320 - palmeres_, 50, 255);
Jd8::blitCK(0, 150, gfx_, 320 - palmeres_, 150, palmeres_, 50, 255);
// "jdes" estàtic (davant dels scrollers) i versió a la cantonada
JD8_BlitCK(87, 167, gfx_, 127, 124, 150, 24, 255);
JD8_BlitCK(303, 193, gfx_, 305, 143, 15, 5, 255);
Jd8::blitCK(87, 167, gfx_, 127, 124, 150, 24, 255);
Jd8::blitCK(303, 193, gfx_, 305, 143, 15, 5, 255);
// "Polsa tecla" parpallejant. Al vell `contador % 100 > 30` amb
// updateTicks=20 ms, el cicle són 2000 ms amb un llindar de 600 ms:
// amagat els primers 600 ms, visible els següents 1400 ms.
const bool blink_on = (blink_ms_ % 2000) > 600;
if (blink_on) {
JD8_BlitCK(98, 130, gfx_, 161, 92, 127, 9, 255);
if (info::ctx.nou_personatge) {
JD8_BlitCK(68, 141, gfx_, 128, 105, 189, 9, 255);
const bool BLINK_ON = (blink_ms_ % 2000) > 600;
if (BLINK_ON) {
Jd8::blitCK(98, 130, gfx_, 161, 92, 127, 9, 255);
if (Info::ctx.nou_personatge) {
Jd8::blitCK(68, 141, gfx_, 128, 105, 189, 9, 255);
}
}
}
void MenuScene::tick(int delta_ms) {
switch (phase_) {
case Phase::FadingIn:
case Phase::FADING_IN:
fade_.tick(delta_ms);
if (fade_.done()) {
phase_ = Phase::Showing;
phase_ = Phase::SHOWING;
}
break;
case Phase::Showing: {
case Phase::SHOWING: {
// Palmeres: 1 pixel cada 80 ms (= cada 4 ticks × 20 ms originals)
palmeres_acc_ms_ += delta_ms;
while (palmeres_acc_ms_ >= 80) {
@@ -96,28 +96,28 @@ namespace scenes {
render();
// Qualsevol tecla tanca el menú. Llegim 'P' explícitament abans
// de reiniciar el flag de input perquè `info::ctx.pepe_activat`
// de reiniciar el flag de input perquè `Info::ctx.pepe_activat`
// reflecteixca si l'usuari estava polsant P al moment d'eixir.
if (JI_AnyKey() || JI_KeyPressed(SDL_SCANCODE_P)) {
info::ctx.pepe_activat = JI_KeyPressed(SDL_SCANCODE_P);
JI_DisableKeyboard(60);
info::ctx.num_piramide = 1;
if (Ji::anyKey() || Ji::keyPressed(SDL_SCANCODE_P)) {
Info::ctx.pepe_activat = Ji::keyPressed(SDL_SCANCODE_P);
Ji::disableKeyboard(60);
Info::ctx.num_piramide = 1;
fade_.startFadeOut();
phase_ = Phase::FadingOut;
phase_ = Phase::FADING_OUT;
}
break;
}
case Phase::FadingOut:
case Phase::FADING_OUT:
fade_.tick(delta_ms);
if (fade_.done()) {
phase_ = Phase::Done;
phase_ = Phase::DONE;
}
break;
case Phase::Done:
case Phase::DONE:
break;
}
}
} // namespace scenes
} // namespace Scenes
+9 -9
View File
@@ -7,7 +7,7 @@
#include "game/scenes/scene.hpp"
#include "game/scenes/surface_handle.hpp"
namespace scenes {
namespace Scenes {
// Menú del títol. Reemplaça `ModuleSequence::doMenu()`.
//
@@ -20,7 +20,7 @@ namespace scenes {
// i el text "polsa tecla" parpallejant cada 2 s (visible 1.4 s,
// amagat 0.6 s, igual que el `contador % 100 > 30` original).
// 4. Quan l'usuari polsa qualsevol tecla — o 'P' per a activar Pepe —
// llegim `info::ctx.pepe_activat`, disparem fade-out i marquem
// llegim `Info::ctx.pepe_activat`, disparem fade-out i marquem
// num_piramide=1 (vas a doSlides).
//
// Registrat al SceneRegistry amb state_key = 0.
@@ -28,13 +28,13 @@ namespace scenes {
public:
void onEnter() override;
void tick(int delta_ms) override;
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::Done; }
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
private:
enum class Phase : std::uint8_t { FadingIn,
Showing,
FadingOut,
Done };
enum class Phase : std::uint8_t { FADING_IN,
SHOWING,
FADING_OUT,
DONE };
void render();
@@ -43,7 +43,7 @@ namespace scenes {
PaletteFade fade_;
FrameAnimator camello_{4, 160, true};
Phase phase_{Phase::FadingIn};
Phase phase_{Phase::FADING_IN};
// Scrollers horizontals. Mouen 1 pixel per pas.
int palmeres_{0};
@@ -55,4 +55,4 @@ namespace scenes {
int blink_ms_{0};
};
} // namespace scenes
} // namespace Scenes
+17 -17
View File
@@ -7,38 +7,38 @@
#include "game/info.hpp"
#include "game/scenes/scene_utils.hpp"
namespace scenes {
namespace Scenes {
void MortScene::onEnter() {
playMusic("music/mort.ogg");
JI_DisableKeyboard(60);
info::ctx.vida = 5;
Ji::disableKeyboard(60);
Info::ctx.vida = 5;
gfx_ = SurfaceHandle("gfx/gameover.gif");
JD8_ClearScreen(0);
JD8_Blit(gfx_);
Jd8::clearScreen(0);
Jd8::blit(gfx_);
// PaletteFade en fa una còpia interna via memcpy, així que alliberem
// la paleta temporal immediatament.
JD8_Palette pal = JD8_LoadPalette("gfx/gameover.gif");
Jd8::Palette pal = Jd8::loadPalette("gfx/gameover.gif");
fade_.startFadeTo(pal);
delete[] pal;
phase_ = Phase::FadingIn;
phase_ = Phase::FADING_IN;
remaining_ms_ = 10000;
}
void MortScene::tick(int delta_ms) {
switch (phase_) {
case Phase::FadingIn:
case Phase::FADING_IN:
fade_.tick(delta_ms);
if (fade_.done()) {
phase_ = Phase::Showing;
phase_ = Phase::SHOWING;
}
break;
case Phase::Showing:
if (JI_AnyKey()) {
case Phase::SHOWING:
if (Ji::anyKey()) {
remaining_ms_ = 0;
} else {
remaining_ms_ -= delta_ms;
@@ -47,22 +47,22 @@ namespace scenes {
// Arrenca música del següent mòdul abans del fade out,
// igual que la versió vella feia al final de doMort().
playMusic("music/menu.ogg");
info::ctx.num_piramide = 0;
Info::ctx.num_piramide = 0;
fade_.startFadeOut();
phase_ = Phase::FadingOut;
phase_ = Phase::FADING_OUT;
}
break;
case Phase::FadingOut:
case Phase::FADING_OUT:
fade_.tick(delta_ms);
if (fade_.done()) {
phase_ = Phase::Done;
phase_ = Phase::DONE;
}
break;
case Phase::Done:
case Phase::DONE:
break;
}
}
} // namespace scenes
} // namespace Scenes
+8 -8
View File
@@ -6,7 +6,7 @@
#include "game/scenes/scene.hpp"
#include "game/scenes/surface_handle.hpp"
namespace scenes {
namespace Scenes {
// Pantalla de "game over". Reemplaça `ModuleSequence::doMort()`.
//
@@ -20,18 +20,18 @@ namespace scenes {
public:
void onEnter() override;
void tick(int delta_ms) override;
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::Done; }
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
private:
enum class Phase : std::uint8_t { FadingIn,
Showing,
FadingOut,
Done };
enum class Phase : std::uint8_t { FADING_IN,
SHOWING,
FADING_OUT,
DONE };
SurfaceHandle gfx_;
PaletteFade fade_;
Phase phase_{Phase::FadingIn};
Phase phase_{Phase::FADING_IN};
int remaining_ms_{10000}; // 1000 ticks × 10 ms/tick del doMort original
};
} // namespace scenes
} // namespace Scenes
+5 -5
View File
@@ -1,14 +1,14 @@
#include "game/scenes/palette_fade.hpp"
namespace scenes {
namespace Scenes {
void PaletteFade::startFadeOut() {
JD8_FadeStartOut();
Jd8::fadeStartOut();
active_ = true;
}
void PaletteFade::startFadeTo(const Color* target) {
JD8_FadeStartToPal(target);
Jd8::fadeStartToPal(target);
active_ = true;
}
@@ -22,9 +22,9 @@ namespace scenes {
// de 500ms exactes independent del framerate") podem convertir la
// màquina d'estats de jdraw8 a time-based ací sense tocar cap altre
// call site.
if (JD8_FadeTickStep()) {
if (Jd8::fadeTickStep()) {
active_ = false;
}
}
} // namespace scenes
} // namespace Scenes
+3 -3
View File
@@ -2,10 +2,10 @@
#include "core/jail/jdraw8.hpp"
namespace scenes {
namespace Scenes {
// Embolcall fi damunt de la màquina d'estats de fade de jdraw8
// (`JD8_FadeStart*` / `JD8_FadeTickStep`). Exposa una API time-based
// (`JD8_FadeStart*` / `Jd8::fadeTickStep`). Exposa una API time-based
// però internament avança un pas del fade per cada crida a `tick()`.
// La raó de tindre-ho com a classe a banda: que una escena no puga
// cridar accidentalment a `JD8_FadeOut`/`JD8_FadeToPal` (els shims
@@ -27,4 +27,4 @@ namespace scenes {
bool active_{false};
};
} // namespace scenes
} // namespace Scenes
+4 -4
View File
@@ -6,7 +6,7 @@
// cert, i llavors consulta `nextState()` per decidir la següent.
//
// Contracte:
// - `tick(delta_ms)` no pot bloquejar ni cridar JD8_Flip — el caller
// - `tick(delta_ms)` no pot bloquejar ni cridar Jd8::flip — el caller
// s'encarrega de fer el flip després del tick.
// - `done()` es consulta just després de cada tick.
// - Els assets són propietat de l'escena (normalment via SurfaceHandle)
@@ -14,7 +14,7 @@
// - `onEnter()` es crida una vegada just abans del primer tick. És el
// moment bo per a arrancar música, disparar un fade-in, etc.
namespace scenes {
namespace Scenes {
class Scene {
public:
@@ -28,10 +28,10 @@ namespace scenes {
// Valor retornat al caller quan l'escena acaba — equivalent al int
// que retornaven les velles funcions `Go()` de ModuleSequence:
// 1 = continuar amb la següent escena segons info::ctx
// 1 = continuar amb la següent escena segons Info::ctx
// 0 = entrar al gameplay (ModuleGame)
// -1 = eixir del joc
[[nodiscard]] virtual auto nextState() const -> int { return 1; }
};
} // namespace scenes
} // namespace Scenes
+7 -7
View File
@@ -1,10 +1,10 @@
#include "game/scenes/scene_registry.hpp"
namespace scenes {
namespace Scenes {
auto SceneRegistry::instance() -> SceneRegistry& {
static SceneRegistry inst;
return inst;
static SceneRegistry instance_;
return instance_;
}
void SceneRegistry::registerScene(int state_key, Factory factory) {
@@ -12,11 +12,11 @@ namespace scenes {
}
auto SceneRegistry::tryCreate(int state_key) const -> std::unique_ptr<Scene> {
const auto it = factories_.find(state_key);
if (it == factories_.end()) {
const auto IT = factories_.find(state_key);
if (IT == factories_.end()) {
return nullptr;
}
return it->second();
return IT->second();
}
} // namespace scenes
} // namespace Scenes
+3 -3
View File
@@ -6,9 +6,9 @@
#include "game/scenes/scene.hpp"
namespace scenes {
namespace Scenes {
// Mapa de `state_key` (actualment = `info::ctx.num_piramide`) a factory
// Mapa de `state_key` (actualment = `Info::ctx.num_piramide`) a factory
// d'escena. Permet que el dispatch de `gameFiberEntry` provi primer una
// Scene nova i caiga al vell `ModuleSequence::Go()` si encara no està
// migrada.
@@ -34,4 +34,4 @@ namespace scenes {
std::unordered_map<int, Factory> factories_;
};
} // namespace scenes
} // namespace Scenes
+2 -2
View File
@@ -4,7 +4,7 @@
#include "core/audio/audio.hpp"
namespace scenes {
namespace Scenes {
namespace {
auto basename(const char* path) -> std::string {
@@ -21,4 +21,4 @@ namespace scenes {
Audio::get()->playMusic(basename(filename), loop);
}
} // namespace scenes
} // namespace Scenes
+2 -2
View File
@@ -3,11 +3,11 @@
// Helpers compartits per les escenes. Aquest header és petit i creix
// quan una abstracció comú apareix en dos o més escenes.
namespace scenes {
namespace Scenes {
// Carrega un OGG de `data/` i arranca'l com a música de fons. Substituïx
// el `play_music()` repetit en tots els doX() del vell modulesequence.
// `loop`: -1 = infinit (per defecte), 0 = una sola vegada, N = N+1 passades.
void playMusic(const char* filename, int loop = -1);
} // namespace scenes
} // namespace Scenes
+49 -49
View File
@@ -12,11 +12,11 @@
namespace {
constexpr int TICK_MS = 20; // JG_SetUpdateTicks(20) del vell doSecreta
constexpr int TICK_MS = 20; // Jg::setUpdateTicks(20) del vell doSecreta
// Durades per fase, derivades dels contador-thresholds del vell:
// tomba1 scroll: 127 passos (contador 1→128) × 20ms
// tomba1 hold: 128 passos (contador 128→0) × 20ms
// Durades per fase, derivades dels CONTADOR-thresholds del vell:
// tomba1 scroll: 127 passos (CONTADOR 1→128) × 20ms
// tomba1 hold: 128 passos (CONTADOR 128→0) × 20ms
// tomba2 scroll: 94 passos × 20ms
// tomba2 hold: 94 passos × 20ms
// reveal horit: 80 passos × 20ms
@@ -34,7 +34,7 @@ namespace {
} // namespace
namespace scenes {
namespace Scenes {
SecretaScene::~SecretaScene() {
delete[] pal_aux_;
@@ -50,156 +50,156 @@ namespace scenes {
fade_.startFadeOut();
gfx_ = SurfaceHandle("gfx/tomba1.gif");
pal_aux_ = JD8_LoadPalette("gfx/tomba1.gif");
pal_aux_ = Jd8::loadPalette("gfx/tomba1.gif");
pal_active_ = new Color[256];
std::memcpy(pal_active_, pal_aux_, 768);
phase_ = Phase::InitialFadeOut;
phase_ = Phase::INITIAL_FADE_OUT;
phase_acc_ms_ = 0;
}
void SecretaScene::swapToTomba2() {
JD8_ClearScreen(255);
Jd8::clearScreen(255);
gfx_.reset("gfx/tomba2.gif");
delete[] pal_aux_;
pal_aux_ = JD8_LoadPalette("gfx/tomba2.gif");
pal_aux_ = Jd8::loadPalette("gfx/tomba2.gif");
// pal_active_ continua sent el mateix buffer: només actualitzem
// el seu contingut. main_palette ja apunta ací.
std::memcpy(pal_active_, pal_aux_, 768);
}
void SecretaScene::beginRedPulseSetup() {
JD8_ClearScreen(0);
JD8_SetPaletteColor(254, 12, 11, 11);
JD8_SetPaletteColor(253, 12, 11, 11);
Jd8::clearScreen(0);
Jd8::setPaletteColor(254, 12, 11, 11);
Jd8::setPaletteColor(253, 12, 11, 11);
}
void SecretaScene::beginFinalFade() {
Audio::get()->fadeOutMusic(250);
fade_.startFadeOut();
phase_ = Phase::FinalFadeOut;
phase_ = Phase::FINAL_FADE_OUT;
}
void SecretaScene::tick(int delta_ms) {
// Skip per tecla (després del fade inicial, no mentre). Salta
// directament al FinalFadeOut. Mateix patró que el vell, on
// directament al FINAL_FADE_OUT. Mateix patró que el vell, on
// qualsevol tecla sortia del loop.
if (!skip_triggered_ && phase_ != Phase::InitialFadeOut && JI_AnyKey()) {
if (!skip_triggered_ && phase_ != Phase::INITIAL_FADE_OUT && Ji::anyKey()) {
skip_triggered_ = true;
beginFinalFade();
}
switch (phase_) {
case Phase::InitialFadeOut:
case Phase::INITIAL_FADE_OUT:
fade_.tick(delta_ms);
if (fade_.done()) {
// Ara main_palette (la vella) té tots els canals a 0.
// SetScreenPalette allibera la vella i adopta pal_active_
// — des d'ara main_palette == pal_active_, així que les
// futures escriptures a pal_active_ afecten la pantalla.
JD8_SetScreenPalette(pal_active_);
JD8_ClearScreen(255);
phase_ = Phase::Tomba1ScrollIn;
Jd8::setScreenPalette(pal_active_);
Jd8::clearScreen(255);
phase_ = Phase::TOMBA1_SCROLL_IN;
phase_acc_ms_ = 0;
}
break;
case Phase::Tomba1ScrollIn: {
case Phase::TOMBA1_SCROLL_IN: {
phase_acc_ms_ += delta_ms;
const int contador = std::min(128, (phase_acc_ms_ / TICK_MS) + 1);
const int CONTADOR = std::min(128, (phase_acc_ms_ / TICK_MS) + 1);
// Dos blits solapats: el primer avança a velocitat completa,
// el segon (contingut de la dreta del src) a meitat (contador>>1).
JD8_Blit(70, 60, gfx_, 0, contador, 178, 70);
JD8_BlitCK(70, 60, gfx_, 178, contador >> 1, 142, 70, 255);
// el segon (contingut de la dreta del src) a meitat (CONTADOR>>1).
Jd8::blit(70, 60, gfx_, 0, CONTADOR, 178, 70);
Jd8::blitCK(70, 60, gfx_, 178, CONTADOR >> 1, 142, 70, 255);
if (phase_acc_ms_ >= TOMBA1_SCROLL_MS) {
phase_ = Phase::Tomba1Hold;
phase_ = Phase::TOMBA1_HOLD;
phase_acc_ms_ = 0;
}
break;
}
case Phase::Tomba1Hold:
case Phase::TOMBA1_HOLD:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= TOMBA1_HOLD_MS) {
swapToTomba2();
phase_ = Phase::Tomba2ScrollIn;
phase_ = Phase::TOMBA2_SCROLL_IN;
phase_acc_ms_ = 0;
}
break;
case Phase::Tomba2ScrollIn: {
case Phase::TOMBA2_SCROLL_IN: {
phase_acc_ms_ += delta_ms;
const int contador = std::min(94, (phase_acc_ms_ / TICK_MS) + 1);
JD8_Blit(55, 53, gfx_, 0, 158 - contador, 211, contador);
const int CONTADOR = std::min(94, (phase_acc_ms_ / TICK_MS) + 1);
Jd8::blit(55, 53, gfx_, 0, 158 - CONTADOR, 211, CONTADOR);
if (phase_acc_ms_ >= TOMBA2_SCROLL_MS) {
phase_ = Phase::Tomba2Hold;
phase_ = Phase::TOMBA2_HOLD;
phase_acc_ms_ = 0;
}
break;
}
case Phase::Tomba2Hold:
case Phase::TOMBA2_HOLD:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= TOMBA2_HOLD_MS) {
beginRedPulseSetup();
phase_ = Phase::Tomba2Reveal;
phase_ = Phase::TOMBA2_REVEAL;
phase_acc_ms_ = 0;
}
break;
case Phase::Tomba2Reveal: {
case Phase::TOMBA2_REVEAL: {
phase_acc_ms_ += delta_ms;
const int contador = std::min(80, (phase_acc_ms_ / TICK_MS) + 1);
const int CONTADOR = std::min(80, (phase_acc_ms_ / TICK_MS) + 1);
// Revelat horitzontal simètric: l'amplada creix 2px per tick
// i el src_x es desplaça a l'esquerra el mateix.
JD8_Blit(80, 68, gfx_, 160 - (contador * 2), 0, contador * 2, 64);
Jd8::blit(80, 68, gfx_, 160 - (CONTADOR * 2), 0, CONTADOR * 2, 64);
if (phase_acc_ms_ >= TOMBA2_REVEAL_MS) {
phase_ = Phase::Tomba2RevealHold;
phase_ = Phase::TOMBA2_REVEAL_HOLD;
phase_acc_ms_ = 0;
}
break;
}
case Phase::Tomba2RevealHold:
case Phase::TOMBA2_REVEAL_HOLD:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= TOMBA2_REVEAL_HOLD_MS) {
phase_ = Phase::RedPulse;
phase_ = Phase::RED_PULSE;
phase_acc_ms_ = 0;
}
break;
case Phase::RedPulse: {
case Phase::RED_PULSE: {
phase_acc_ms_ += delta_ms;
const int contador = std::min(51, phase_acc_ms_ / TICK_MS);
const int CONTADOR = std::min(51, phase_acc_ms_ / TICK_MS);
// Anima el canal R dels índexs 254 i 253 (aquest a la meitat
// de brillantor). Va de (12,11,11) fins a (63,11,11) / (31,11,11).
JD8_SetPaletteColor(254, contador + 12, 11, 11);
JD8_SetPaletteColor(253, (contador + 12) >> 1, 11, 11);
Jd8::setPaletteColor(254, CONTADOR + 12, 11, 11);
Jd8::setPaletteColor(253, (CONTADOR + 12) >> 1, 11, 11);
if (phase_acc_ms_ >= RED_PULSE_MS) {
phase_ = Phase::RedPulseHold;
phase_ = Phase::RED_PULSE_HOLD;
phase_acc_ms_ = 0;
}
break;
}
case Phase::RedPulseHold:
case Phase::RED_PULSE_HOLD:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= RED_PULSE_HOLD_MS) {
beginFinalFade();
}
break;
case Phase::FinalFadeOut:
case Phase::FINAL_FADE_OUT:
fade_.tick(delta_ms);
if (fade_.done()) {
phase_ = Phase::Done;
phase_ = Phase::DONE;
}
break;
case Phase::Done:
case Phase::DONE:
break;
}
}
} // namespace scenes
} // namespace Scenes
+18 -18
View File
@@ -7,7 +7,7 @@
#include "game/scenes/scene.hpp"
#include "game/scenes/surface_handle.hpp"
namespace scenes {
namespace Scenes {
// Pre-Secreta. Reemplaça `ModuleSequence::doSecreta()`.
//
@@ -22,7 +22,7 @@ namespace scenes {
// i pinta un revelat horitzontal (~1.6 s + ~1.6 s de pausa).
// 5. "Red pulse": anima els colors 253/254 incrementant el canal R
// de 12 a 62 durant ~1 s (+ ~1 s de pausa).
// 6. FadeOut + JA_FadeOutMusic(250).
// 6. FadeOut + Ja::fadeOutMusic(250).
// 7. Retorna nextState=0 per entrar al ModuleGame amb num_piramide=6.
//
// Registrada al SceneRegistry amb state_key = 6.
@@ -33,22 +33,22 @@ namespace scenes {
void onEnter() override;
void tick(int delta_ms) override;
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::Done; }
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
[[nodiscard]] auto nextState() const -> int override { return 0; }
private:
enum class Phase : std::uint8_t {
InitialFadeOut,
Tomba1ScrollIn,
Tomba1Hold,
Tomba2ScrollIn,
Tomba2Hold,
Tomba2Reveal,
Tomba2RevealHold,
RedPulse,
RedPulseHold,
FinalFadeOut,
Done,
INITIAL_FADE_OUT,
TOMBA1_SCROLL_IN,
TOMBA1_HOLD,
TOMBA2_SCROLL_IN,
TOMBA2_HOLD,
TOMBA2_REVEAL,
TOMBA2_REVEAL_HOLD,
RED_PULSE,
RED_PULSE_HOLD,
FINAL_FADE_OUT,
DONE,
};
void swapToTomba2();
@@ -56,13 +56,13 @@ namespace scenes {
void beginFinalFade();
SurfaceHandle gfx_;
JD8_Palette pal_aux_{nullptr};
JD8_Palette pal_active_{nullptr}; // propietat transferida a main_palette
Jd8::Palette pal_aux_{nullptr};
Jd8::Palette pal_active_{nullptr}; // propietat transferida a main_palette
PaletteFade fade_;
Phase phase_{Phase::InitialFadeOut};
Phase phase_{Phase::INITIAL_FADE_OUT};
int phase_acc_ms_{0};
bool skip_triggered_{false};
};
} // namespace scenes
} // namespace Scenes
+103 -96
View File
@@ -20,15 +20,15 @@ namespace {
constexpr int BG_COLOR_INDEX = 255;
// Desplaçament inicial del slide segons direcció del wipe.
// Slide 1 i 3: "scroll in from right" (pos_x va de 320 → 0).
// Slide 2: "wipe reverse" (pos_x va de -320 → 0), el mateix efecte
// Slide 1 i 3: "scroll in from right" (POS_X va de 320 → 0).
// Slide 2: "wipe reverse" (POS_X va de -320 → 0), el mateix efecte
// estrany del doSlides vell on la imatge es desplaça des de l'esquerra
// però revela primer el lateral dret del src.
constexpr int SLIDE_START_X[3] = {320, -320, 320};
} // namespace
namespace scenes {
namespace Scenes {
SlidesScene::~SlidesScene() {
delete[] pal_aux_;
@@ -36,19 +36,19 @@ namespace scenes {
}
void SlidesScene::onEnter() {
num_piramide_at_start_ = info::ctx.num_piramide;
num_piramide_at_start_ = Info::ctx.num_piramide;
const char* arxiu = nullptr;
if (num_piramide_at_start_ == 7) {
// loop=1 per replicar el vell `play_music("final.ogg", 1)`.
playMusic("music/final.ogg", 1);
arxiu = (info::ctx.diners < 200) ? "gfx/intro2.gif" : "gfx/intro3.gif";
arxiu = (Info::ctx.diners < 200) ? "gfx/intro2.gif" : "gfx/intro3.gif";
} else {
arxiu = "gfx/intro.gif";
}
gfx_ = SurfaceHandle(arxiu);
pal_aux_ = JD8_LoadPalette(arxiu);
pal_aux_ = Jd8::loadPalette(arxiu);
// Còpia editable de la paleta. `pal_active_` comparteix memòria amb
// main_palette després del SetScreenPalette — modificar-la modifica
@@ -56,19 +56,19 @@ namespace scenes {
// restaurar després de cada fade-out intermedi.
pal_active_ = new Color[256];
std::memcpy(pal_active_, pal_aux_, 768);
JD8_SetScreenPalette(pal_active_);
Jd8::setScreenPalette(pal_active_);
JD8_ClearScreen(BG_COLOR_INDEX);
Jd8::clearScreen(BG_COLOR_INDEX);
phase_ = Phase::Slide1Enter;
phase_ = Phase::SLIDE1_ENTER;
phase_acc_ms_ = 0;
next_state_ = 0;
}
void SlidesScene::drawSlide(int slide_idx, int pos_x) {
const int src_y = slide_idx * SLIDE_H;
const int SRC_Y = slide_idx * SLIDE_H;
// Clipping manual: translada un rect de 320×65 des de (pos_x, SLIDE_Y)
// Clipping manual: translada un rect de 320×65 des de (POS_X, SLIDE_Y)
// a l'àrea visible (0..319, SLIDE_Y..SLIDE_Y+64).
int dst_x = pos_x;
int src_x = 0;
@@ -83,7 +83,7 @@ namespace scenes {
}
if (w > 0) {
JD8_Blit(dst_x, SLIDE_Y, gfx_, src_x, src_y, w, SLIDE_H);
Jd8::blit(dst_x, SLIDE_Y, gfx_, src_x, SRC_Y, w, SLIDE_H);
}
}
@@ -96,105 +96,112 @@ namespace scenes {
Audio::get()->fadeOutMusic(250);
}
fade_.startFadeOut();
phase_ = Phase::FadeFinal;
phase_ = Phase::FADE_FINAL;
}
void SlidesScene::triggerSkip() {
skip_triggered_ = true;
if (num_piramide_at_start_ != 7) {
Audio::get()->fadeOutMusic(250);
}
fade_.startFadeOut();
phase_ = Phase::FADE_FINAL;
}
void SlidesScene::tickSlideEnter(int delta_ms) {
phase_acc_ms_ += delta_ms;
int slide_idx = 2;
if (phase_ == Phase::SLIDE1_ENTER) {
slide_idx = 0;
} else if (phase_ == Phase::SLIDE2_ENTER) {
slide_idx = 1;
}
const float T = std::min(1.0F, static_cast<float>(phase_acc_ms_) / static_cast<float>(SCROLL_MS));
const float EASED = Easing::outCubic(T);
drawSlide(slide_idx, Easing::lerpInt(SLIDE_START_X[slide_idx], 0, EASED));
if (phase_acc_ms_ < SCROLL_MS) {
return;
}
drawSlide(slide_idx, 0);
switch (phase_) {
case Phase::SLIDE1_ENTER:
phase_ = Phase::SLIDE1_HOLD;
break;
case Phase::SLIDE2_ENTER:
phase_ = Phase::SLIDE2_HOLD;
break;
default:
phase_ = Phase::SLIDE3_HOLD;
break;
}
phase_acc_ms_ = 0;
}
void SlidesScene::tickHoldIntermediate(int delta_ms) {
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ < HOLD_MS) {
return;
}
fade_.startFadeOut();
phase_ = (phase_ == Phase::SLIDE1_HOLD) ? Phase::FADE_OUT1 : Phase::FADE_OUT2;
phase_acc_ms_ = 0;
}
void SlidesScene::tickFadeOutIntermediate(int delta_ms) {
fade_.tick(delta_ms);
if (!fade_.done()) {
return;
}
restorePalette();
Jd8::clearScreen(BG_COLOR_INDEX);
phase_ = (phase_ == Phase::FADE_OUT1) ? Phase::SLIDE2_ENTER : Phase::SLIDE3_ENTER;
phase_acc_ms_ = 0;
}
void SlidesScene::tickFinalFade(int delta_ms) {
fade_.tick(delta_ms);
if (!fade_.done()) {
return;
}
if (num_piramide_at_start_ == 7) {
Info::ctx.num_piramide = 8;
next_state_ = 1;
} else {
next_state_ = 0;
}
phase_ = Phase::DONE;
}
void SlidesScene::tick(int delta_ms) {
// Skip: qualsevol tecla salta directament al fade final. Per fidelitat
// al vell doSlides, el skip NO atura la música explícitament — només
// el final natural crida JA_FadeOutMusic (beginFinalFade() distingeix).
if (!skip_triggered_ && JI_AnyKey()) {
skip_triggered_ = true;
if (num_piramide_at_start_ != 7) {
Audio::get()->fadeOutMusic(250);
}
fade_.startFadeOut();
phase_ = Phase::FadeFinal;
if (!skip_triggered_ && Ji::anyKey()) {
triggerSkip();
}
switch (phase_) {
case Phase::Slide1Enter:
case Phase::Slide2Enter:
case Phase::Slide3Enter: {
phase_acc_ms_ += delta_ms;
int slide_idx = 2;
if (phase_ == Phase::Slide1Enter) {
slide_idx = 0;
} else if (phase_ == Phase::Slide2Enter) {
slide_idx = 1;
}
const float t = std::min(1.0F, static_cast<float>(phase_acc_ms_) / static_cast<float>(SCROLL_MS));
const float eased = Easing::outCubic(t);
const int pos_x = Easing::lerpInt(SLIDE_START_X[slide_idx], 0, eased);
drawSlide(slide_idx, pos_x);
if (phase_acc_ms_ >= SCROLL_MS) {
// Garanteix posició final exacta (pos_x=0).
drawSlide(slide_idx, 0);
if (phase_ == Phase::Slide1Enter) {
phase_ = Phase::Slide1Hold;
} else if (phase_ == Phase::Slide2Enter) {
phase_ = Phase::Slide2Hold;
} else {
phase_ = Phase::Slide3Hold;
}
phase_acc_ms_ = 0;
}
case Phase::SLIDE1_ENTER:
case Phase::SLIDE2_ENTER:
case Phase::SLIDE3_ENTER:
tickSlideEnter(delta_ms);
break;
}
case Phase::Slide1Hold:
case Phase::Slide2Hold:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= HOLD_MS) {
fade_.startFadeOut();
if (phase_ == Phase::Slide1Hold) {
phase_ = Phase::FadeOut1;
} else {
phase_ = Phase::FadeOut2;
}
phase_acc_ms_ = 0;
}
case Phase::SLIDE1_HOLD:
case Phase::SLIDE2_HOLD:
tickHoldIntermediate(delta_ms);
break;
case Phase::Slide3Hold:
case Phase::SLIDE3_HOLD:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= HOLD_MS) {
beginFinalFade();
}
break;
case Phase::FadeOut1:
case Phase::FadeOut2:
fade_.tick(delta_ms);
if (fade_.done()) {
restorePalette();
JD8_ClearScreen(BG_COLOR_INDEX);
if (phase_ == Phase::FadeOut1) {
phase_ = Phase::Slide2Enter;
} else {
phase_ = Phase::Slide3Enter;
}
phase_acc_ms_ = 0;
}
case Phase::FADE_OUT1:
case Phase::FADE_OUT2:
tickFadeOutIntermediate(delta_ms);
break;
case Phase::FadeFinal:
fade_.tick(delta_ms);
if (fade_.done()) {
if (num_piramide_at_start_ == 7) {
info::ctx.num_piramide = 8;
next_state_ = 1;
} else {
next_state_ = 0;
}
phase_ = Phase::Done;
}
case Phase::FADE_FINAL:
tickFinalFade(delta_ms);
break;
case Phase::Done:
case Phase::DONE:
break;
}
}
} // namespace scenes
} // namespace Scenes
+32 -26
View File
@@ -7,7 +7,7 @@
#include "game/scenes/scene.hpp"
#include "game/scenes/surface_handle.hpp"
namespace scenes {
namespace Scenes {
// 3 slides narratius amb scroll d'entrada + espera + transició amb
// fade-out. Reemplaça `ModuleSequence::doSlides()`.
@@ -18,18 +18,18 @@ namespace scenes {
// - altre cas (num_piramide == 1): gfx/intro.gif, sense música nova
//
// Flux:
// Slide1Enter (1600 ms scroll dreta→centre, easing outCubic)
// → Slide1Hold (4600 ms)
// → FadeOut1 + clear + reset paleta
// → Slide2Enter (1600 ms scroll esquerra→centre)
// → Slide2Hold (4600 ms)
// → FadeOut2 + clear + reset paleta
// → Slide3Enter (1600 ms scroll dreta→centre)
// → Slide3Hold (4600 ms)
// → FadeFinal (JA_FadeOutMusic si num_piramide != 7 + fade paleta)
// SLIDE1_ENTER (1600 ms scroll dreta→centre, easing outCubic)
// → SLIDE1_HOLD (4600 ms)
// → FADE_OUT1 + clear + reset paleta
// → SLIDE2_ENTER (1600 ms scroll esquerra→centre)
// → SLIDE2_HOLD (4600 ms)
// → FADE_OUT2 + clear + reset paleta
// → SLIDE3_ENTER (1600 ms scroll dreta→centre)
// → SLIDE3_HOLD (4600 ms)
// → FADE_FINAL (Ja::fadeOutMusic si num_piramide != 7 + fade paleta)
// → Done
//
// Qualsevol tecla salta directament a FadeFinal (sense cortar la música
// Qualsevol tecla salta directament a FADE_FINAL (sense cortar la música
// si hem entrat per num_piramide==7, per fidelitat al vell).
//
// NextState:
@@ -42,21 +42,21 @@ namespace scenes {
void onEnter() override;
void tick(int delta_ms) override;
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::Done; }
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
[[nodiscard]] auto nextState() const -> int override { return next_state_; }
private:
enum class Phase : std::uint8_t {
Slide1Enter,
Slide1Hold,
FadeOut1,
Slide2Enter,
Slide2Hold,
FadeOut2,
Slide3Enter,
Slide3Hold,
FadeFinal,
Done,
SLIDE1_ENTER,
SLIDE1_HOLD,
FADE_OUT1,
SLIDE2_ENTER,
SLIDE2_HOLD,
FADE_OUT2,
SLIDE3_ENTER,
SLIDE3_HOLD,
FADE_FINAL,
DONE,
};
// Pinta un slide amb desplaçament horitzontal. `slide_idx` = 0..2
@@ -65,17 +65,23 @@ namespace scenes {
void drawSlide(int slide_idx, int pos_x);
void restorePalette();
void beginFinalFade();
// Helpers per a `tick()` — extrets per reduir complexitat cognitiva.
void triggerSkip();
void tickSlideEnter(int delta_ms);
void tickHoldIntermediate(int delta_ms);
void tickFadeOutIntermediate(int delta_ms);
void tickFinalFade(int delta_ms);
SurfaceHandle gfx_;
JD8_Palette pal_aux_{nullptr}; // còpia "neta" que preservem
JD8_Palette pal_active_{nullptr}; // propietat transferida a main_palette
Jd8::Palette pal_aux_{nullptr}; // còpia "neta" que preservem
Jd8::Palette pal_active_{nullptr}; // propietat transferida a main_palette
PaletteFade fade_;
Phase phase_{Phase::Slide1Enter};
Phase phase_{Phase::SLIDE1_ENTER};
int phase_acc_ms_{0};
int num_piramide_at_start_{1};
int next_state_{0};
bool skip_triggered_{false};
};
} // namespace scenes
} // namespace Scenes
+6 -6
View File
@@ -2,7 +2,7 @@
#include <algorithm>
namespace scenes {
namespace Scenes {
void SpriteMover::moveTo(int x0, int y0, int x1, int y1, int duration_ms, EaseFn ease) {
x0_ = x0;
@@ -32,10 +32,10 @@ namespace scenes {
return;
}
elapsed_ms_ = std::min(elapsed_ms_ + delta_ms, duration_ms_);
const float t = static_cast<float>(elapsed_ms_) / static_cast<float>(duration_ms_);
const float eased = ease_(t);
cur_x_ = Easing::lerpInt(x0_, x1_, eased);
cur_y_ = Easing::lerpInt(y0_, y1_, eased);
const float T = static_cast<float>(elapsed_ms_) / static_cast<float>(duration_ms_);
const float EASED = ease_(T);
cur_x_ = Easing::lerpInt(x0_, x1_, EASED);
cur_y_ = Easing::lerpInt(y0_, y1_, EASED);
}
auto SpriteMover::progress() const -> float {
@@ -45,4 +45,4 @@ namespace scenes {
return static_cast<float>(elapsed_ms_) / static_cast<float>(duration_ms_);
}
} // namespace scenes
} // namespace Scenes
+2 -2
View File
@@ -2,7 +2,7 @@
#include "utils/easing.hpp"
namespace scenes {
namespace Scenes {
// Interpola una posició 2D entre dos punts durant un temps donat amb
// una funció d'easing. No toca cap surface — el caller llegix x()/y()
@@ -35,4 +35,4 @@ namespace scenes {
EaseFn ease_{Easing::linear};
};
} // namespace scenes
} // namespace Scenes
+11 -11
View File
@@ -1,13 +1,13 @@
#include "game/scenes/surface_handle.hpp"
namespace scenes {
namespace Scenes {
SurfaceHandle::SurfaceHandle(const char* file)
: surface_(JD8_LoadSurface(file)) {}
: surface_(Jd8::loadSurface(file)) {}
SurfaceHandle::~SurfaceHandle() {
if (surface_ != nullptr) {
JD8_FreeSurface(surface_);
Jd8::freeSurface(surface_);
}
}
@@ -19,7 +19,7 @@ namespace scenes {
auto SurfaceHandle::operator=(SurfaceHandle&& other) noexcept -> SurfaceHandle& {
if (this != &other) {
if (surface_ != nullptr) {
JD8_FreeSurface(surface_);
Jd8::freeSurface(surface_);
}
surface_ = other.surface_;
other.surface_ = nullptr;
@@ -29,22 +29,22 @@ namespace scenes {
void SurfaceHandle::reset(const char* file) {
if (surface_ != nullptr) {
JD8_FreeSurface(surface_);
Jd8::freeSurface(surface_);
}
surface_ = (file != nullptr) ? JD8_LoadSurface(file) : nullptr;
surface_ = (file != nullptr) ? Jd8::loadSurface(file) : nullptr;
}
void SurfaceHandle::adopt(JD8_Surface raw) {
void SurfaceHandle::adopt(Jd8::Surface raw) {
if (surface_ != nullptr) {
JD8_FreeSurface(surface_);
Jd8::freeSurface(surface_);
}
surface_ = raw;
}
auto SurfaceHandle::release() -> JD8_Surface {
JD8_Surface r = surface_;
auto SurfaceHandle::release() -> Jd8::Surface {
Jd8::Surface r = surface_;
surface_ = nullptr;
return r;
}
} // namespace scenes
} // namespace Scenes
+14 -14
View File
@@ -2,12 +2,12 @@
#include "core/jail/jdraw8.hpp"
namespace scenes {
namespace Scenes {
// Wrapper RAII damunt de `JD8_Surface`. Allibera automàticament amb
// `JD8_FreeSurface` al destructor. Move-only per evitar dobles alliberaments.
// Converteix implícitament a `JD8_Surface` per a poder passar-lo
// directament a `JD8_Blit*` sense haver de cridar `.get()`.
// Wrapper RAII damunt de `Jd8::Surface`. Allibera automàticament amb
// `Jd8::freeSurface` al destructor. Move-only per evitar dobles alliberaments.
// Converteix implícitament a `Jd8::Surface` per a poder passar-lo
// directament a `Jd8::blit*` sense haver de cridar `.get()`.
class SurfaceHandle {
public:
SurfaceHandle() = default;
@@ -25,25 +25,25 @@ namespace scenes {
// (p.ex. doSecreta que passa de tomba1 a tomba2).
void reset(const char* file);
// Adopta una surface ja creada (p.ex. amb JD8_NewSurface). Pren ownership
// Adopta una surface ja creada (p.ex. amb Jd8::newSurface). Pren ownership
// — la surface adoptada s'allibera al destructor o al següent reset/adopt.
void adopt(JD8_Surface raw);
void adopt(Jd8::Surface raw);
// Allibera ownership sense destruir la surface. Retorna el pointer cru;
// el caller passa a ser responsable d'alliberar-lo (o de passar-lo a un
// altre propietari). Usat quan una escena delega a codi legacy que
// també allibera la mateixa surface — cal "soltar" el ownership per
// evitar double free.
[[nodiscard]] auto release() -> JD8_Surface;
[[nodiscard]] auto release() -> Jd8::Surface;
// Conversió implícita per al confort d'ús: JD8_Blit(handle)
// en lloc de JD8_Blit(handle.get()).
operator JD8_Surface() const { return surface_; }
[[nodiscard]] auto get() const -> JD8_Surface { return surface_; }
// Conversió implícita per al confort d'ús: Jd8::blit(handle)
// en lloc de Jd8::blit(handle.get()).
operator Jd8::Surface() const { return surface_; }
[[nodiscard]] auto get() const -> Jd8::Surface { return surface_; }
[[nodiscard]] auto valid() const -> bool { return surface_ != nullptr; }
private:
JD8_Surface surface_{nullptr};
Jd8::Surface surface_{nullptr};
};
} // namespace scenes
} // namespace Scenes
+4 -4
View File
@@ -2,7 +2,7 @@
#include <algorithm>
namespace scenes {
namespace Scenes {
auto Timeline::step(int duration_ms, StepFn fn) -> Timeline& {
Step s;
@@ -63,9 +63,9 @@ namespace scenes {
// Pot ser que el següent pas siga una cadena de one-shots.
flushOneShots();
} else if (s.continuous) {
const float p = static_cast<float>(elapsed_in_step_) /
const float P = static_cast<float>(elapsed_in_step_) /
static_cast<float>(std::max(1, s.duration_ms));
s.continuous(p);
s.continuous(P);
}
}
@@ -98,4 +98,4 @@ namespace scenes {
return static_cast<float>(elapsed_in_step_) / static_cast<float>(s.duration_ms);
}
} // namespace scenes
} // namespace Scenes
+4 -4
View File
@@ -3,16 +3,16 @@
#include <functional>
#include <vector>
namespace scenes {
namespace Scenes {
// Timeline declaratiu de passos seqüencials. Cada pas té una duració en
// ms i un callback. Exemple d'ús:
//
// timeline_
// .once([this] { JD8_ClearScreen(0); fade_.startFadeTo(pal); })
// .once([this] { Jd8::clearScreen(0); fade_.startFadeTo(pal); })
// .step(5000) // espera pura
// .step(1000, [this](float p) { /*...*/ }) // animat amb progress
// .once([this] { JA_FadeOutMusic(250); });
// .once([this] { Ja::fadeOutMusic(250); });
//
// `tick(delta_ms)` avança el temps. Els passos one-shot s'executen al
// moment d'entrar-hi i avancen immediatament. Els passos amb duració
@@ -54,4 +54,4 @@ namespace scenes {
bool skipped_{false};
};
} // namespace scenes
} // namespace Scenes
+3 -3
View File
@@ -1,9 +1,9 @@
#include "game/sprite.hpp"
Sprite::Sprite(JD8_Surface gfx)
: gfx(gfx) {}
Sprite::Sprite(Jd8::Surface gfx)
: gfx_(gfx) {}
void Sprite::draw() {
const Frame& f = entitat.frames[entitat.animacions[o].frames[cur_frame]];
JD8_BlitCK(x, y, gfx, f.x, f.y, f.w, f.h, 255);
Jd8::blitCK(x, y, gfx_, f.x, f.y, f.w, f.h, 255);
}
+3 -3
View File
@@ -22,7 +22,7 @@ struct Entitat {
class Sprite {
public:
explicit Sprite(JD8_Surface gfx);
explicit Sprite(Jd8::Surface gfx);
virtual ~Sprite() = default;
virtual void draw();
@@ -34,6 +34,6 @@ class Sprite {
Uint16 o = 0;
protected:
JD8_Surface gfx;
Uint8 cycles_per_frame = 1;
Jd8::Surface gfx_;
Uint8 cycles_per_frame_ = 1;
};
+17 -17
View File
@@ -30,7 +30,7 @@ auto SDL_AppInit(void** /*appstate*/, int /*argc*/, char* /*argv*/[]) -> SDL_App
srand(unsigned(time(nullptr)));
// Crea la carpeta de configuració i carrega les opcions
file_setconfigfolder("jailgames/aee");
Jf::setConfigFolder("jailgames/aee");
// Ruta absoluta a data/ basada en la ubicació de l'executable.
// SDL_GetBasePath() detecta automàticament si estem dins d'un .app bundle
@@ -38,8 +38,8 @@ auto SDL_AppInit(void** /*appstate*/, int /*argc*/, char* /*argv*/[]) -> SDL_App
const char* base_path = SDL_GetBasePath();
std::string resource_pack_path;
if (base_path != nullptr) {
const std::string data_path = std::string(base_path) + "data/";
file_setresourcefolder(data_path.c_str());
const std::string DATA_PATH = std::string(base_path) + "data/";
Jf::setResourceFolder(DATA_PATH.c_str());
resource_pack_path = std::string(base_path) + "resources.pack";
} else {
resource_pack_path = "resources.pack";
@@ -49,25 +49,25 @@ auto SDL_AppInit(void** /*appstate*/, int /*argc*/, char* /*argv*/[]) -> SDL_App
// Release natiu exigix el pack (sense fallback); Debug i WASM mantenen
// el fallback actiu per a desenvolupament i per al build amb MEMFS.
#if defined(NDEBUG) && !defined(__EMSCRIPTEN__)
const bool enable_fallback = false;
const bool ENABLE_FALLBACK = false;
#else
const bool enable_fallback = true;
const bool ENABLE_FALLBACK = true;
#endif
if (!ResourceHelper::initializeResourceSystem(resource_pack_path, enable_fallback)) {
if (!ResourceHelper::initializeResourceSystem(resource_pack_path, ENABLE_FALLBACK)) {
return SDL_APP_FAILURE;
}
Options::setConfigFile(std::string(file_getconfigfolder()) + "config.yaml");
Options::setConfigFile(std::string(Jf::getConfigFolder()) + "config.yaml");
Options::loadFromFile();
// KeyConfig: defaults des de data/input/keys.yaml + overrides de l'usuari
KeyConfig::init("input/keys.yaml",
std::string(file_getconfigfolder()) + "keys.yaml");
std::string(Jf::getConfigFolder()) + "keys.yaml");
#ifndef NDEBUG
// debug.yaml: estat inicial de gameplay per a tests ràpids,
// només en builds de debug.
Options::setDebugFile(std::string(file_getconfigfolder()) + "debug.yaml");
Options::setDebugFile(std::string(Jf::getConfigFolder()) + "debug.yaml");
Options::loadDebugFromFile();
#endif
@@ -84,15 +84,15 @@ auto SDL_AppInit(void** /*appstate*/, int /*argc*/, char* /*argv*/[]) -> SDL_App
Locale::load("locale/ca.yaml");
// Carrega presets de shaders
Options::setPostFXFile(std::string(file_getconfigfolder()) + "postfx.yaml");
Options::setPostFXFile(std::string(Jf::getConfigFolder()) + "postfx.yaml");
Options::loadPostFXFromFile();
Options::setCrtPiFile(std::string(file_getconfigfolder()) + "crtpi.yaml");
Options::setCrtPiFile(std::string(Jf::getConfigFolder()) + "crtpi.yaml");
Options::loadCrtPiFromFile();
JG_Init();
Jg::init();
Screen::init();
JD8_Init();
Audio::init(); // crida internament JA_Init i aplica Options::audio
Jd8::init();
Audio::init(); // crida internament Ja::init i aplica Options::audio
Overlay::init();
Menu::init();
@@ -146,9 +146,9 @@ void SDL_AppQuit(void* /*appstate*/, SDL_AppResult /*result*/) {
Overlay::destroy();
Resource::Cache::destroy();
Resource::List::destroy();
Audio::destroy(); // el destructor del singleton crida JA_Quit
JD8_Quit();
Audio::destroy(); // el destructor del singleton crida Ja::quit
Jd8::quit();
Screen::destroy();
JG_Finalize();
Jg::finalize();
ResourceHelper::shutdownResourceSystem();
}
+2 -2
View File
@@ -11,8 +11,8 @@ namespace Easing {
}
auto outCubic(float t) -> float {
const float inv = 1.0F - t;
return 1.0F - (inv * inv * inv);
const float INV = 1.0F - t;
return 1.0F - (INV * INV * INV);
}
auto inCubic(float t) -> float { return t * t * t; }