scenes: infraestructura de la capa scenes:: (scene, timeline, sprite mover, frame animator, palette fade, surface handle, registry)
This commit is contained in:
@@ -41,6 +41,14 @@ set(APP_SOURCES
|
|||||||
source/core/system/director.cpp
|
source/core/system/director.cpp
|
||||||
source/core/system/fiber.cpp
|
source/core/system/fiber.cpp
|
||||||
|
|
||||||
|
# Scenes (cinemàtiques i menús reescrits)
|
||||||
|
source/scenes/timeline.cpp
|
||||||
|
source/scenes/sprite_mover.cpp
|
||||||
|
source/scenes/frame_animator.cpp
|
||||||
|
source/scenes/palette_fade.cpp
|
||||||
|
source/scenes/surface_handle.cpp
|
||||||
|
source/scenes/scene_registry.cpp
|
||||||
|
|
||||||
# Game
|
# Game
|
||||||
source/game/options.cpp
|
source/game/options.cpp
|
||||||
source/game/bola.cpp
|
source/game/bola.cpp
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
#include "game/modulegame.hpp"
|
#include "game/modulegame.hpp"
|
||||||
#include "game/modulesequence.hpp"
|
#include "game/modulesequence.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
|
#include "scenes/scene.hpp"
|
||||||
|
#include "scenes/scene_registry.hpp"
|
||||||
|
|
||||||
// Cheats del joc original — declarats a jinput.cpp
|
// Cheats del joc original — declarats a jinput.cpp
|
||||||
extern void JI_moveCheats(Uint8 new_key);
|
extern void JI_moveCheats(Uint8 new_key);
|
||||||
@@ -50,6 +52,26 @@ void gameFiberEntry() {
|
|||||||
|
|
||||||
int gameState = 1;
|
int gameState = 1;
|
||||||
while (gameState != -1 && !JG_Quitting()) {
|
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) {
|
switch (gameState) {
|
||||||
case 0: {
|
case 0: {
|
||||||
auto* moduleGame = new ModuleGame();
|
auto* moduleGame = new ModuleGame();
|
||||||
@@ -95,198 +117,225 @@ void Director::togglePause() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Director::run() {
|
void Director::setup() {
|
||||||
// Doble buffer: game_frame és el frame net del joc, presentation_buffer
|
// Els buffers són membres (director.hpp); només els inicialitzem.
|
||||||
// és el frame + overlay (es regenera cada iteració des de game_frame).
|
std::memset(game_frame_, 0, sizeof(game_frame_));
|
||||||
// El doble buffer encara té sentit perquè el Director pot presentar
|
std::memset(presentation_buffer_, 0, sizeof(presentation_buffer_));
|
||||||
// més frames que els que genera el joc (p.ex. durant pauses o mentre
|
has_frame_ = false;
|
||||||
// el joc està en un "wait" intern que triga milisegons).
|
}
|
||||||
Uint32 game_frame[320 * 200]{};
|
|
||||||
Uint32 presentation_buffer[320 * 200]{};
|
bool Director::iterate() {
|
||||||
bool has_frame = false;
|
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_VSYNC = 16; // ~60 FPS amb VSync
|
||||||
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
|
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
|
||||||
|
|
||||||
while (!GameFiber::is_done() && !quit_requested_) {
|
const Uint32 frame_start = SDL_GetTicks();
|
||||||
Uint32 frame_start = SDL_GetTicks();
|
|
||||||
|
|
||||||
handleEvents();
|
Gamepad::update();
|
||||||
Gamepad::update();
|
KeyRemap::update();
|
||||||
KeyRemap::update();
|
GlobalInputs::handle();
|
||||||
GlobalInputs::handle();
|
Mouse::updateCursorVisibility();
|
||||||
Mouse::updateCursorVisibility();
|
|
||||||
|
|
||||||
// Bombeig de l'àudio: reomple l'stream de música i para els canals
|
// 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ó
|
// drenats. Substituïx el callback de SDL_AddTimer de la versió
|
||||||
// antiga — imprescindible per al futur port a emscripten.
|
// antiga — imprescindible per al port a emscripten.
|
||||||
JA_Update();
|
JA_Update();
|
||||||
|
|
||||||
// Dispara els crèdits cinematogràfics la primera vegada que el joc
|
// Dispara els crèdits cinematogràfics la primera vegada que el joc
|
||||||
// arriba al menú del títol (info::ctx.num_piramide == 0).
|
// arriba al menú del títol (info::ctx.num_piramide == 0).
|
||||||
static bool credits_triggered = false;
|
static bool credits_triggered = false;
|
||||||
if (!credits_triggered && info::ctx.num_piramide == 0) {
|
if (!credits_triggered && info::ctx.num_piramide == 0) {
|
||||||
if (Options::game.show_title_credits) {
|
if (Options::game.show_title_credits) {
|
||||||
Overlay::startCredits();
|
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);
|
|
||||||
}
|
}
|
||||||
|
credits_triggered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si el joc encara no ha acabat (p.ex. eixida per ESC doble-press),
|
// Si l'overlay ja no bloqueja ESC (timeout), desbloquegem
|
||||||
// li donem l'oportunitat de tornar net: marquem quit i reprenem el
|
if (esc_blocked_ && !Overlay::isEscConsumed()) {
|
||||||
// fiber fins que detecte JG_Quitting() i retorne de forma natural.
|
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();
|
JG_QuitSignal();
|
||||||
while (!GameFiber::is_done()) {
|
while (!GameFiber::is_done()) {
|
||||||
GameFiber::resume();
|
GameFiber::resume();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Director::handleEvents() {
|
void Director::run() {
|
||||||
|
setup();
|
||||||
|
while (true) {
|
||||||
|
pollAllEvents();
|
||||||
|
if (!iterate()) break;
|
||||||
|
}
|
||||||
|
teardown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Director::pollAllEvents() {
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
while (SDL_PollEvent(&event)) {
|
while (SDL_PollEvent(&event)) {
|
||||||
if (event.type == SDL_EVENT_QUIT) {
|
handleEvent(event);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
void Director::requestQuit() {
|
||||||
quit_requested_ = true;
|
quit_requested_ = true;
|
||||||
JG_QuitSignal();
|
JG_QuitSignal();
|
||||||
|
|||||||
@@ -17,11 +17,22 @@ class Director {
|
|||||||
static void destroy();
|
static void destroy();
|
||||||
static auto get() -> Director*;
|
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();
|
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)
|
// Demana l'eixida (ex: segona pulsació d'ESC o SDL_QUIT)
|
||||||
void requestQuit();
|
void requestQuit();
|
||||||
|
auto isQuitRequested() const -> bool { return quit_requested_; }
|
||||||
|
|
||||||
// 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;
|
auto consumeKeyPressed() -> bool;
|
||||||
@@ -40,7 +51,14 @@ class Director {
|
|||||||
|
|
||||||
static Director* instance_;
|
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> quit_requested_{false};
|
||||||
std::atomic<bool> key_pressed_{false};
|
std::atomic<bool> key_pressed_{false};
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
|
// Port a l'API de callbacks de SDL3: el runtime posseïx el main loop i ens
|
||||||
|
// crida a SDL_AppInit/SDL_AppIterate/SDL_AppEvent/SDL_AppQuit. Imprescindible
|
||||||
|
// per al port a emscripten on no podem tindre un bucle while propi al hilo
|
||||||
|
// principal. Funciona igual en build natiu (Linux/macOS/Windows) perquè
|
||||||
|
// SDL3 embolcalla el seu propi main loop darrere d'aquestes callbacks.
|
||||||
|
|
||||||
|
#define SDL_MAIN_USE_CALLBACKS
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3/SDL_main.h>
|
||||||
|
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -14,8 +22,8 @@
|
|||||||
#include "core/system/director.hpp"
|
#include "core/system/director.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
|
|
||||||
int main(int /*argc*/, char* /*args*/[]) {
|
SDL_AppResult SDL_AppInit(void** /*appstate*/, int /*argc*/, char* /*argv*/[]) {
|
||||||
srand(unsigned(time(NULL)));
|
srand(unsigned(time(nullptr)));
|
||||||
|
|
||||||
// Crea la carpeta de configuració i carrega les opcions
|
// Crea la carpeta de configuració i carrega les opcions
|
||||||
file_setconfigfolder("jailgames/aee");
|
file_setconfigfolder("jailgames/aee");
|
||||||
@@ -25,7 +33,7 @@ int main(int /*argc*/, char* /*args*/[]) {
|
|||||||
// (retorna Contents/Resources/) o en un executable normal (carpeta del binari).
|
// (retorna Contents/Resources/) o en un executable normal (carpeta del binari).
|
||||||
const char* base_path = SDL_GetBasePath();
|
const char* base_path = SDL_GetBasePath();
|
||||||
if (base_path) {
|
if (base_path) {
|
||||||
std::string data_path = std::string(base_path) + "data/";
|
const std::string data_path = std::string(base_path) + "data/";
|
||||||
file_setresourcefolder(data_path.c_str());
|
file_setresourcefolder(data_path.c_str());
|
||||||
}
|
}
|
||||||
Options::setConfigFile(std::string(file_getconfigfolder()) + "config.yaml");
|
Options::setConfigFile(std::string(file_getconfigfolder()) + "config.yaml");
|
||||||
@@ -48,9 +56,33 @@ int main(int /*argc*/, char* /*args*/[]) {
|
|||||||
Overlay::init();
|
Overlay::init();
|
||||||
Menu::init();
|
Menu::init();
|
||||||
Director::init();
|
Director::init();
|
||||||
|
Director::get()->setup();
|
||||||
|
|
||||||
// Arranca el Director: crea game thread, bucle principal, sincronització de frames
|
return SDL_APP_CONTINUE;
|
||||||
Director::get()->run();
|
}
|
||||||
|
|
||||||
|
SDL_AppResult SDL_AppIterate(void* /*appstate*/) {
|
||||||
|
// Una iteració del bucle del Director. Abans els events es drenaven
|
||||||
|
// amb SDL_PollEvent dins d'aquesta funció; ara SDL ens els lliura
|
||||||
|
// d'un en un via SDL_AppEvent, així que iterate() no els toca.
|
||||||
|
if (!Director::get()->iterate()) {
|
||||||
|
return SDL_APP_SUCCESS;
|
||||||
|
}
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_AppResult SDL_AppEvent(void* /*appstate*/, SDL_Event* event) {
|
||||||
|
if (!event) return SDL_APP_CONTINUE;
|
||||||
|
Director::get()->handleEvent(*event);
|
||||||
|
if (Director::get()->isQuitRequested()) {
|
||||||
|
return SDL_APP_SUCCESS;
|
||||||
|
}
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL_AppQuit(void* /*appstate*/, SDL_AppResult /*result*/) {
|
||||||
|
// Neteja en ordre invers al de SDL_AppInit.
|
||||||
|
Director::get()->teardown();
|
||||||
|
|
||||||
Options::saveToFile();
|
Options::saveToFile();
|
||||||
|
|
||||||
@@ -61,6 +93,4 @@ int main(int /*argc*/, char* /*args*/[]) {
|
|||||||
JD8_Quit();
|
JD8_Quit();
|
||||||
Screen::destroy();
|
Screen::destroy();
|
||||||
JG_Finalize();
|
JG_Finalize();
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
36
source/scenes/frame_animator.cpp
Normal file
36
source/scenes/frame_animator.cpp
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#include "scenes/frame_animator.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace scenes {
|
||||||
|
|
||||||
|
FrameAnimator::FrameAnimator(int num_frames, int frame_ms, bool loop)
|
||||||
|
: num_frames_(std::max(1, num_frames)),
|
||||||
|
frame_ms_(std::max(1, frame_ms)),
|
||||||
|
loop_(loop) {}
|
||||||
|
|
||||||
|
void FrameAnimator::tick(int delta_ms) {
|
||||||
|
if (finished_) return;
|
||||||
|
elapsed_ms_ += delta_ms;
|
||||||
|
while (elapsed_ms_ >= frame_ms_) {
|
||||||
|
elapsed_ms_ -= frame_ms_;
|
||||||
|
++current_frame_;
|
||||||
|
if (current_frame_ >= num_frames_) {
|
||||||
|
if (loop_) {
|
||||||
|
current_frame_ = 0;
|
||||||
|
} else {
|
||||||
|
current_frame_ = num_frames_ - 1;
|
||||||
|
finished_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FrameAnimator::reset() {
|
||||||
|
current_frame_ = 0;
|
||||||
|
elapsed_ms_ = 0;
|
||||||
|
finished_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace scenes
|
||||||
34
source/scenes/frame_animator.hpp
Normal file
34
source/scenes/frame_animator.hpp
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
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 —
|
||||||
|
// només el caller sap quins frames dibuixar a partir de `frame()`.
|
||||||
|
//
|
||||||
|
// Usat per animacions periòdiques amb frames subsamplejats: palmeres,
|
||||||
|
// camell, aigua, torxes, Sam caminant amb `(i/5) % fr` del codi original.
|
||||||
|
class FrameAnimator {
|
||||||
|
public:
|
||||||
|
FrameAnimator() = default;
|
||||||
|
FrameAnimator(int num_frames, int frame_ms, bool loop = true);
|
||||||
|
|
||||||
|
void tick(int delta_ms);
|
||||||
|
|
||||||
|
int frame() const { return current_frame_; }
|
||||||
|
bool done() const { return !loop_ && finished_; }
|
||||||
|
int numFrames() const { return num_frames_; }
|
||||||
|
|
||||||
|
void reset();
|
||||||
|
void setFrameMs(int frame_ms) { frame_ms_ = frame_ms; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
int num_frames_{1};
|
||||||
|
int frame_ms_{100};
|
||||||
|
bool loop_{true};
|
||||||
|
int current_frame_{0};
|
||||||
|
int elapsed_ms_{0};
|
||||||
|
bool finished_{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace scenes
|
||||||
28
source/scenes/palette_fade.cpp
Normal file
28
source/scenes/palette_fade.cpp
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#include "scenes/palette_fade.hpp"
|
||||||
|
|
||||||
|
namespace scenes {
|
||||||
|
|
||||||
|
void PaletteFade::startFadeOut() {
|
||||||
|
JD8_FadeStartOut();
|
||||||
|
active_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PaletteFade::startFadeTo(JD8_Palette target) {
|
||||||
|
JD8_FadeStartToPal(target);
|
||||||
|
active_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PaletteFade::tick(int /*delta_ms*/) {
|
||||||
|
if (!active_) return;
|
||||||
|
// El fade té 32 passos interns. Amb un tick per frame (~16ms)
|
||||||
|
// dura ~512ms — el mateix temps que la versió bloquejant original.
|
||||||
|
// Si en el futur volem fer-lo genuinament time-based (p.ex. "fade
|
||||||
|
// 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()) {
|
||||||
|
active_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace scenes
|
||||||
30
source/scenes/palette_fade.hpp
Normal file
30
source/scenes/palette_fade.hpp
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/jail/jdraw8.hpp"
|
||||||
|
|
||||||
|
namespace scenes {
|
||||||
|
|
||||||
|
// Embolcall fi damunt de la màquina d'estats de fade de jdraw8
|
||||||
|
// (`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
|
||||||
|
// bloquejants vells) i que el `done()` siga consultable com la resta
|
||||||
|
// dels helpers.
|
||||||
|
class PaletteFade {
|
||||||
|
public:
|
||||||
|
PaletteFade() = default;
|
||||||
|
|
||||||
|
void startFadeOut();
|
||||||
|
void startFadeTo(JD8_Palette target);
|
||||||
|
|
||||||
|
void tick(int delta_ms);
|
||||||
|
|
||||||
|
bool active() const { return active_; }
|
||||||
|
bool done() const { return !active_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool active_{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace scenes
|
||||||
37
source/scenes/scene.hpp
Normal file
37
source/scenes/scene.hpp
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Interfície base per a una escena (cinemàtica, menú, banner, etc.) del
|
||||||
|
// joc. Una escena és una unitat autònoma amb un `tick(delta_ms)` no
|
||||||
|
// bloquejant. El Director la fa avançar cada frame fins que `done()` és
|
||||||
|
// cert, i llavors consulta `nextState()` per decidir la següent.
|
||||||
|
//
|
||||||
|
// Contracte:
|
||||||
|
// - `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)
|
||||||
|
// i s'alliberen al destructor.
|
||||||
|
// - `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 {
|
||||||
|
|
||||||
|
class Scene {
|
||||||
|
public:
|
||||||
|
virtual ~Scene() = default;
|
||||||
|
|
||||||
|
virtual void onEnter() {}
|
||||||
|
|
||||||
|
virtual void tick(int delta_ms) = 0;
|
||||||
|
|
||||||
|
virtual bool done() const = 0;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// 0 = entrar al gameplay (ModuleGame)
|
||||||
|
// -1 = eixir del joc
|
||||||
|
virtual int nextState() const { return 1; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace scenes
|
||||||
20
source/scenes/scene_registry.cpp
Normal file
20
source/scenes/scene_registry.cpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#include "scenes/scene_registry.hpp"
|
||||||
|
|
||||||
|
namespace scenes {
|
||||||
|
|
||||||
|
SceneRegistry& SceneRegistry::instance() {
|
||||||
|
static SceneRegistry inst;
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneRegistry::registerScene(int state_key, Factory factory) {
|
||||||
|
factories_[state_key] = std::move(factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Scene> SceneRegistry::tryCreate(int state_key) const {
|
||||||
|
const auto it = factories_.find(state_key);
|
||||||
|
if (it == factories_.end()) return nullptr;
|
||||||
|
return it->second();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace scenes
|
||||||
37
source/scenes/scene_registry.hpp
Normal file
37
source/scenes/scene_registry.hpp
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "scenes/scene.hpp"
|
||||||
|
|
||||||
|
namespace scenes {
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Registre inicial: `Director::init()` cridarà `instance()` i afegirà
|
||||||
|
// una entrada per cada escena ja portada. A mesura que vagen caient, les
|
||||||
|
// línies del registre creixen i les funcions `doX()` del modulesequence
|
||||||
|
// desapareixen.
|
||||||
|
class SceneRegistry {
|
||||||
|
public:
|
||||||
|
using Factory = std::function<std::unique_ptr<Scene>()>;
|
||||||
|
|
||||||
|
static SceneRegistry& instance();
|
||||||
|
|
||||||
|
void registerScene(int state_key, Factory factory);
|
||||||
|
|
||||||
|
// Retorna `nullptr` si no hi ha cap escena registrada per a aquest
|
||||||
|
// state. El caller hauria de caure al path legacy en aquest cas.
|
||||||
|
std::unique_ptr<Scene> tryCreate(int state_key) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
SceneRegistry() = default;
|
||||||
|
std::unordered_map<int, Factory> factories_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace scenes
|
||||||
46
source/scenes/sprite_mover.cpp
Normal file
46
source/scenes/sprite_mover.cpp
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#include "scenes/sprite_mover.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace scenes {
|
||||||
|
|
||||||
|
void SpriteMover::moveTo(int x0, int y0, int x1, int y1, int duration_ms, EaseFn ease) {
|
||||||
|
x0_ = x0;
|
||||||
|
y0_ = y0;
|
||||||
|
x1_ = x1;
|
||||||
|
y1_ = y1;
|
||||||
|
duration_ms_ = std::max(0, duration_ms);
|
||||||
|
elapsed_ms_ = 0;
|
||||||
|
ease_ = ease ? ease : Easing::linear;
|
||||||
|
cur_x_ = x0;
|
||||||
|
cur_y_ = y0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpriteMover::setPosition(int x, int y) {
|
||||||
|
cur_x_ = x;
|
||||||
|
cur_y_ = y;
|
||||||
|
x0_ = x1_ = x;
|
||||||
|
y0_ = y1_ = y;
|
||||||
|
duration_ms_ = 0;
|
||||||
|
elapsed_ms_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpriteMover::tick(int delta_ms) {
|
||||||
|
if (duration_ms_ <= 0) {
|
||||||
|
cur_x_ = x1_;
|
||||||
|
cur_y_ = y1_;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
float SpriteMover::progress() const {
|
||||||
|
if (duration_ms_ <= 0) return 1.0f;
|
||||||
|
return static_cast<float>(elapsed_ms_) / static_cast<float>(duration_ms_);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace scenes
|
||||||
39
source/scenes/sprite_mover.hpp
Normal file
39
source/scenes/sprite_mover.hpp
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "utils/easing.hpp"
|
||||||
|
|
||||||
|
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()
|
||||||
|
// i fa el blit ell mateix. Pensat per a scrolls, sprites que entren/
|
||||||
|
// ixen per pantalla, i moviments d'objectes interpolats.
|
||||||
|
class SpriteMover {
|
||||||
|
public:
|
||||||
|
using EaseFn = float (*)(float);
|
||||||
|
|
||||||
|
SpriteMover() = default;
|
||||||
|
|
||||||
|
// Arrenca un moviment nou. Si ja n'hi havia un en curs, es descarta.
|
||||||
|
void moveTo(int x0, int y0, int x1, int y1, int duration_ms,
|
||||||
|
EaseFn ease = Easing::linear);
|
||||||
|
|
||||||
|
// Posicionament immediat (útil per a "teleportar" entre moviments).
|
||||||
|
void setPosition(int x, int y);
|
||||||
|
|
||||||
|
void tick(int delta_ms);
|
||||||
|
|
||||||
|
int x() const { return cur_x_; }
|
||||||
|
int y() const { return cur_y_; }
|
||||||
|
bool done() const { return elapsed_ms_ >= duration_ms_; }
|
||||||
|
float progress() const; // 0..1 sense easing aplicat
|
||||||
|
|
||||||
|
private:
|
||||||
|
int x0_{0}, y0_{0}, x1_{0}, y1_{0};
|
||||||
|
int duration_ms_{0};
|
||||||
|
int elapsed_ms_{0};
|
||||||
|
int cur_x_{0}, cur_y_{0};
|
||||||
|
EaseFn ease_{Easing::linear};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace scenes
|
||||||
31
source/scenes/surface_handle.cpp
Normal file
31
source/scenes/surface_handle.cpp
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#include "scenes/surface_handle.hpp"
|
||||||
|
|
||||||
|
namespace scenes {
|
||||||
|
|
||||||
|
SurfaceHandle::SurfaceHandle(const char* file)
|
||||||
|
: surface_(JD8_LoadSurface(file)) {}
|
||||||
|
|
||||||
|
SurfaceHandle::~SurfaceHandle() {
|
||||||
|
if (surface_) JD8_FreeSurface(surface_);
|
||||||
|
}
|
||||||
|
|
||||||
|
SurfaceHandle::SurfaceHandle(SurfaceHandle&& other) noexcept
|
||||||
|
: surface_(other.surface_) {
|
||||||
|
other.surface_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SurfaceHandle& SurfaceHandle::operator=(SurfaceHandle&& other) noexcept {
|
||||||
|
if (this != &other) {
|
||||||
|
if (surface_) JD8_FreeSurface(surface_);
|
||||||
|
surface_ = other.surface_;
|
||||||
|
other.surface_ = nullptr;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SurfaceHandle::reset(const char* file) {
|
||||||
|
if (surface_) JD8_FreeSurface(surface_);
|
||||||
|
surface_ = file ? JD8_LoadSurface(file) : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace scenes
|
||||||
38
source/scenes/surface_handle.hpp
Normal file
38
source/scenes/surface_handle.hpp
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/jail/jdraw8.hpp"
|
||||||
|
|
||||||
|
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()`.
|
||||||
|
class SurfaceHandle {
|
||||||
|
public:
|
||||||
|
SurfaceHandle() = default;
|
||||||
|
explicit SurfaceHandle(const char* file);
|
||||||
|
~SurfaceHandle();
|
||||||
|
|
||||||
|
SurfaceHandle(const SurfaceHandle&) = delete;
|
||||||
|
SurfaceHandle& operator=(const SurfaceHandle&) = delete;
|
||||||
|
|
||||||
|
SurfaceHandle(SurfaceHandle&& other) noexcept;
|
||||||
|
SurfaceHandle& operator=(SurfaceHandle&& other) noexcept;
|
||||||
|
|
||||||
|
// Allibera la surface actual (si n'hi ha) i carrega una nova.
|
||||||
|
// Usat per escenes que recarreguen assets a mitja cinemàtica
|
||||||
|
// (p.ex. doSecreta que passa de tomba1 a tomba2).
|
||||||
|
void reset(const char* file);
|
||||||
|
|
||||||
|
// Conversió implícita per al confort d'ús: JD8_Blit(handle)
|
||||||
|
// en lloc de JD8_Blit(handle.get()).
|
||||||
|
operator JD8_Surface() const { return surface_; }
|
||||||
|
JD8_Surface get() const { return surface_; }
|
||||||
|
bool valid() const { return surface_ != nullptr; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
JD8_Surface surface_{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace scenes
|
||||||
85
source/scenes/timeline.cpp
Normal file
85
source/scenes/timeline.cpp
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#include "scenes/timeline.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace scenes {
|
||||||
|
|
||||||
|
Timeline& Timeline::step(int duration_ms, StepFn fn) {
|
||||||
|
Step s;
|
||||||
|
s.duration_ms = duration_ms;
|
||||||
|
s.continuous = std::move(fn);
|
||||||
|
steps_.push_back(std::move(s));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Timeline& Timeline::once(OnceFn fn) {
|
||||||
|
Step s;
|
||||||
|
s.duration_ms = 0;
|
||||||
|
s.oneshot = std::move(fn);
|
||||||
|
steps_.push_back(std::move(s));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Timeline::flushOneShots() {
|
||||||
|
while (current_ < steps_.size() && steps_[current_].duration_ms == 0) {
|
||||||
|
auto& s = steps_[current_];
|
||||||
|
if (!s.entered) {
|
||||||
|
s.entered = true;
|
||||||
|
if (s.oneshot) s.oneshot();
|
||||||
|
}
|
||||||
|
++current_;
|
||||||
|
elapsed_in_step_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Timeline::tick(int delta_ms) {
|
||||||
|
if (skipped_) return;
|
||||||
|
flushOneShots();
|
||||||
|
if (current_ >= steps_.size()) return;
|
||||||
|
|
||||||
|
auto& s = steps_[current_];
|
||||||
|
if (!s.entered) {
|
||||||
|
s.entered = true;
|
||||||
|
// Primer tick dins del pas: cridem amb progress=0 si hi ha callback.
|
||||||
|
if (s.continuous) s.continuous(0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed_in_step_ += delta_ms;
|
||||||
|
if (elapsed_in_step_ >= s.duration_ms) {
|
||||||
|
// Tancament del pas: una crida final amb progress=1.
|
||||||
|
if (s.continuous) s.continuous(1.0f);
|
||||||
|
++current_;
|
||||||
|
elapsed_in_step_ = 0;
|
||||||
|
// 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_) /
|
||||||
|
static_cast<float>(std::max(1, s.duration_ms));
|
||||||
|
s.continuous(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Timeline::skip() {
|
||||||
|
skipped_ = true;
|
||||||
|
current_ = steps_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Timeline::reset() {
|
||||||
|
for (auto& s : steps_) s.entered = false;
|
||||||
|
current_ = 0;
|
||||||
|
elapsed_in_step_ = 0;
|
||||||
|
skipped_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Timeline::done() const {
|
||||||
|
return skipped_ || current_ >= steps_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
float Timeline::currentProgress() const {
|
||||||
|
if (current_ >= steps_.size()) return 1.0f;
|
||||||
|
const auto& s = steps_[current_];
|
||||||
|
if (s.duration_ms <= 0) return 0.0f;
|
||||||
|
return static_cast<float>(elapsed_in_step_) / static_cast<float>(s.duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace scenes
|
||||||
57
source/scenes/timeline.hpp
Normal file
57
source/scenes/timeline.hpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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); })
|
||||||
|
// .step(5000) // espera pura
|
||||||
|
// .step(1000, [this](float p) { /*...*/ }) // animat amb progress
|
||||||
|
// .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ó
|
||||||
|
// criden el seu callback cada tick amb el progress [0..1] i passen al
|
||||||
|
// següent quan s'exhaureix el temps. `skip()` marca tota la timeline
|
||||||
|
// com a acabada (no executa res més) — útil per als "polsa una tecla
|
||||||
|
// per a saltar la cinemàtica".
|
||||||
|
class Timeline {
|
||||||
|
public:
|
||||||
|
using StepFn = std::function<void(float progress_0_1)>;
|
||||||
|
using OnceFn = std::function<void()>;
|
||||||
|
|
||||||
|
Timeline& step(int duration_ms, StepFn fn = nullptr);
|
||||||
|
Timeline& once(OnceFn fn);
|
||||||
|
|
||||||
|
void tick(int delta_ms);
|
||||||
|
void skip();
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
bool done() const;
|
||||||
|
int currentStepIndex() const { return static_cast<int>(current_); }
|
||||||
|
float currentProgress() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Step {
|
||||||
|
int duration_ms{0}; // 0 = one-shot
|
||||||
|
StepFn continuous;
|
||||||
|
OnceFn oneshot;
|
||||||
|
bool entered{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Avança els one-shots consecutius des de `current_` fins a trobar
|
||||||
|
// un pas amb duració > 0 o l'endoll de la llista.
|
||||||
|
void flushOneShots();
|
||||||
|
|
||||||
|
std::vector<Step> steps_;
|
||||||
|
std::size_t current_{0};
|
||||||
|
int elapsed_in_step_{0};
|
||||||
|
bool skipped_{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace scenes
|
||||||
Reference in New Issue
Block a user