scenes: infraestructura de la capa scenes:: (scene, timeline, sprite mover, frame animator, palette fade, surface handle, registry)

This commit is contained in:
2026-04-15 19:40:39 +02:00
parent 1507a1c740
commit 4436f7f569
17 changed files with 803 additions and 180 deletions

View File

@@ -20,6 +20,8 @@
#include "game/modulegame.hpp"
#include "game/modulesequence.hpp"
#include "game/options.hpp"
#include "scenes/scene.hpp"
#include "scenes/scene_registry.hpp"
// Cheats del joc original — declarats a jinput.cpp
extern void JI_moveCheats(Uint8 new_key);
@@ -50,6 +52,26 @@ void gameFiberEntry() {
int gameState = 1;
while (gameState != -1 && !JG_Quitting()) {
// Mode "Scene nova": si el state actual és de seqüència i el
// registry té una escena migrada per al num_piramide, l'executem
// amb un mini-loop tick-based. El codi de la Scene no toca
// fibers; el JD8_Flip() entre ticks és el que cedeix al Director.
if (gameState == 1) {
if (auto scene = scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide)) {
scene->onEnter();
Uint32 last = SDL_GetTicks();
while (!scene->done() && !JG_Quitting()) {
const Uint32 now = SDL_GetTicks();
scene->tick(static_cast<int>(now - last));
last = now;
JD8_Flip(); // presenta i cedix al Director
}
gameState = scene->nextState();
continue;
}
}
// Fallback al codi legacy (encara no migrat a Scene).
switch (gameState) {
case 0: {
auto* moduleGame = new ModuleGame();
@@ -95,198 +117,225 @@ void Director::togglePause() {
}
}
void Director::run() {
// Doble buffer: game_frame és el frame net del joc, presentation_buffer
// és el frame + overlay (es regenera cada iteració des de game_frame).
// El doble buffer encara té sentit perquè el Director pot presentar
// més frames que els que genera el joc (p.ex. durant pauses o mentre
// el joc està en un "wait" intern que triga milisegons).
Uint32 game_frame[320 * 200]{};
Uint32 presentation_buffer[320 * 200]{};
bool has_frame = false;
void Director::setup() {
// Els buffers són membres (director.hpp); només els inicialitzem.
std::memset(game_frame_, 0, sizeof(game_frame_));
std::memset(presentation_buffer_, 0, sizeof(presentation_buffer_));
has_frame_ = false;
}
bool Director::iterate() {
if (GameFiber::is_done() || quit_requested_) {
// Si el joc encara no ha acabat (p.ex. eixida per ESC doble-press),
// li donem l'oportunitat de tornar net: marquem quit i reprenem el
// fiber fins que detecte JG_Quitting() i retorne de forma natural.
JG_QuitSignal();
while (!GameFiber::is_done()) {
GameFiber::resume();
}
return false;
}
constexpr Uint32 FRAME_MS_VSYNC = 16; // ~60 FPS amb VSync
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
while (!GameFiber::is_done() && !quit_requested_) {
Uint32 frame_start = SDL_GetTicks();
const Uint32 frame_start = SDL_GetTicks();
handleEvents();
Gamepad::update();
KeyRemap::update();
GlobalInputs::handle();
Mouse::updateCursorVisibility();
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 futur port a emscripten.
JA_Update();
// 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.
JA_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;
}
// Si l'overlay ja no bloqueja ESC (timeout), desbloquegem
if (esc_blocked_ && !Overlay::isEscConsumed()) {
esc_blocked_ = false;
}
// Cedeix el control al fiber del joc. Quan retorne (per un
// JD8_Flip() dins del joc) tindrem un nou frame a pixel_data.
// Si estem en pausa, no executem el fiber: es queda congelat al
// seu últim yield i continuem presentant l'últim frame conegut.
if (!paused_) {
GameFiber::resume();
if (GameFiber::is_done()) break;
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) {
memcpy(presentation_buffer, game_frame, sizeof(presentation_buffer));
Screen::get()->present(presentation_buffer);
}
// Límit de framerate segons VSync
Uint32 target_ms = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC;
Uint32 elapsed = SDL_GetTicks() - frame_start;
if (elapsed < target_ms) {
SDL_Delay(target_ms - elapsed);
// 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;
}
// Si el joc encara no ha acabat (p.ex. eixida per ESC doble-press),
// li donem l'oportunitat de tornar net: marquem quit i reprenem el
// fiber fins que detecte JG_Quitting() i retorne de forma natural.
// Si l'overlay ja no bloqueja ESC (timeout), desbloquegem
if (esc_blocked_ && !Overlay::isEscConsumed()) {
esc_blocked_ = false;
}
// Cedeix el control al fiber del joc. Quan retorne (per un JD8_Flip()
// dins del joc) tindrem un nou frame a pixel_data. Si estem en pausa,
// no executem el fiber: es queda congelat al seu últim yield i
// continuem presentant l'últim frame conegut.
if (!paused_) {
GameFiber::resume();
if (GameFiber::is_done()) {
return false;
}
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);
}
return true;
}
void Director::teardown() {
// Si el joc encara no ha acabat (p.ex. eixida per SDL_QUIT des del
// sistema), li donem l'oportunitat de tornar net.
JG_QuitSignal();
while (!GameFiber::is_done()) {
GameFiber::resume();
}
}
void Director::handleEvents() {
void Director::run() {
setup();
while (true) {
pollAllEvents();
if (!iterate()) break;
}
teardown();
}
void Director::pollAllEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_QUIT) {
JG_QuitSignal();
requestQuit();
}
// Hot-plug de gamepad
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) {
Gamepad::handleEvent(event);
continue;
}
// Salta els crèdits amb qualsevol tecla; no deixem que arribi al joc
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
Overlay::cancelCredits();
continue;
}
// 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;
continue;
}
// 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;
continue;
}
// Pausa: F11 (o tecla configurada) pausa/reprén la simulació
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
event.key.scancode == Options::keys_gui.pause_toggle) {
togglePause();
Overlay::showNotification(paused_ ? Locale::get("notifications.pause") : Locale::get("notifications.resume"));
menu_keys_held_[event.key.scancode] = true;
continue;
}
// Menú: F12 (o tecla configurada) obre/tanca el menú flotant
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
event.key.scancode == Options::keys_gui.menu_toggle) {
Menu::toggle();
JI_SetInputBlocked(Menu::isOpen());
menu_keys_held_[event.key.scancode] = true;
continue;
}
// 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;
continue;
}
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
continue; // no deixem passar KEY_UP al joc tampoc
}
// 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;
continue;
}
// 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;
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;
}
continue; // 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
continue;
} else {
// Comprova si és una tecla GUI (no passa al joc)
const auto sc = event.key.scancode;
const bool is_gui_key = (sc == Options::keys_gui.dec_zoom ||
sc == Options::keys_gui.inc_zoom ||
sc == Options::keys_gui.fullscreen ||
sc == Options::keys_gui.toggle_shader ||
sc == Options::keys_gui.toggle_aspect_ratio ||
sc == Options::keys_gui.toggle_supersampling ||
sc == Options::keys_gui.next_shader ||
sc == Options::keys_gui.next_shader_preset ||
sc == Options::keys_gui.toggle_stretch_filter ||
sc == Options::keys_gui.toggle_render_info);
if (!is_gui_key) {
key_pressed_ = true;
JI_moveCheats(sc);
}
}
}
Mouse::handleEvent(event);
handleEvent(event);
}
}
void Director::handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_QUIT) {
JG_QuitSignal();
requestQuit();
}
// Hot-plug de gamepad
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) {
Gamepad::handleEvent(event);
return;
}
// Salta els crèdits amb qualsevol tecla; no deixem que arribi al joc
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
Overlay::cancelCredits();
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;
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;
return;
}
// Pausa: F11 (o tecla configurada) pausa/reprén la simulació
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
event.key.scancode == Options::keys_gui.pause_toggle) {
togglePause();
Overlay::showNotification(paused_ ? Locale::get("notifications.pause") : Locale::get("notifications.resume"));
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 == Options::keys_gui.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
}
// 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;
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;
} else {
// Comprova si és una tecla GUI (no passa al joc)
const auto sc = event.key.scancode;
const bool is_gui_key = (sc == Options::keys_gui.dec_zoom ||
sc == Options::keys_gui.inc_zoom ||
sc == Options::keys_gui.fullscreen ||
sc == Options::keys_gui.toggle_shader ||
sc == Options::keys_gui.toggle_aspect_ratio ||
sc == Options::keys_gui.toggle_supersampling ||
sc == Options::keys_gui.next_shader ||
sc == Options::keys_gui.next_shader_preset ||
sc == Options::keys_gui.toggle_stretch_filter ||
sc == Options::keys_gui.toggle_render_info);
if (!is_gui_key) {
key_pressed_ = true;
JI_moveCheats(sc);
}
}
}
Mouse::handleEvent(event);
}
void Director::requestQuit() {
quit_requested_ = true;
JG_QuitSignal();

View File

@@ -17,11 +17,22 @@ class Director {
static void destroy();
static auto get() -> Director*;
// Bucle principal del director. Crida des de main().
// Bucle principal clàssic (build natiu sense SDL_MAIN_USE_CALLBACKS).
// Internament crida setup() + bucle d'iterate() + teardown(). Crida des de main().
void run();
// Punts d'entrada compatibles amb SDL_AppInit / SDL_AppIterate /
// SDL_AppEvent / SDL_AppQuit. Permeten que el Director siga driven
// per l'event loop de SDL3 en lloc d'un bucle propi — imprescindible
// per al port a emscripten, on el runtime posseïx el main loop.
void setup();
bool iterate(); // torna false quan el joc vol eixir
void teardown();
void handleEvent(const SDL_Event& event);
// Demana l'eixida (ex: segona pulsació d'ESC o SDL_QUIT)
void requestQuit();
auto isQuitRequested() const -> bool { return quit_requested_; }
// Consumeix el flag de "tecla polsada" (com l'antic JI_AnyKey)
auto consumeKeyPressed() -> bool;
@@ -40,7 +51,14 @@ class Director {
static Director* instance_;
void handleEvents();
void pollAllEvents(); // drenatge amb SDL_PollEvent, només per al bucle natiu
// Buffers persistents entre iteracions. Abans eren locals a run(),
// ara són membres perquè iterate() els pot reutilitzar sense tornar-los
// a reservar en cada crida del callback.
Uint32 game_frame_[320 * 200]{};
Uint32 presentation_buffer_[320 * 200]{};
bool has_frame_{false};
std::atomic<bool> quit_requested_{false};
std::atomic<bool> key_pressed_{false};