388 lines
14 KiB
C++
388 lines
14 KiB
C++
#include "core/system/director.hpp"
|
|
|
|
#include <cstring>
|
|
#include <iostream>
|
|
|
|
#include "core/input/gamepad.hpp"
|
|
#include "core/input/global_inputs.hpp"
|
|
#include "core/input/key_remap.hpp"
|
|
#include "core/input/mouse.hpp"
|
|
#include "core/jail/jail_audio.hpp"
|
|
#include "core/jail/jdraw8.hpp"
|
|
#include "core/jail/jgame.hpp"
|
|
#include "core/jail/jinput.hpp"
|
|
#include "core/locale/locale.hpp"
|
|
#include "core/rendering/menu.hpp"
|
|
#include "core/rendering/overlay.hpp"
|
|
#include "core/rendering/screen.hpp"
|
|
#include "core/system/fiber.hpp"
|
|
#include "game/info.hpp"
|
|
#include "game/modulegame.hpp"
|
|
#include "game/options.hpp"
|
|
#include "scenes/banner_scene.hpp"
|
|
#include "scenes/credits_scene.hpp"
|
|
#include "scenes/intro_new_logo_scene.hpp"
|
|
#include "scenes/intro_scene.hpp"
|
|
#include "scenes/menu_scene.hpp"
|
|
#include "scenes/mort_scene.hpp"
|
|
#include "scenes/scene.hpp"
|
|
#include "scenes/scene_registry.hpp"
|
|
#include "scenes/secreta_scene.hpp"
|
|
#include "scenes/slides_scene.hpp"
|
|
|
|
// Cheats del joc original — declarats a jinput.cpp
|
|
extern void JI_moveCheats(Uint8 new_key);
|
|
|
|
Director* Director::instance_ = nullptr;
|
|
|
|
namespace {
|
|
|
|
// Entry del fiber del joc. Alterna entre la capa `scenes::` (cinemàtiques
|
|
// i menús, triats per `info::ctx.num_piramide` via el SceneRegistry) i
|
|
// ModuleGame (gameplay) fins que el joc demana eixir. Quan el codi de
|
|
// gameplay o una escena crida JD8_Flip(), el control torna automàticament
|
|
// al Director via `GameFiber::yield()`.
|
|
void gameFiberEntry() {
|
|
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;
|
|
fclose(ini);
|
|
}
|
|
|
|
int gameState = 1;
|
|
while (gameState != -1 && !JG_Quitting()) {
|
|
if (gameState == 0) {
|
|
// Gameplay pur (ModuleGame encara és cooperatiu-clàssic: conté
|
|
// el seu while intern i crida JD8_Flip manualment). Fora
|
|
// d'abast de la migració scenes:: — es tractarà en una fase
|
|
// posterior quan el fiber es puga eliminar.
|
|
auto* mg = new ModuleGame();
|
|
gameState = mg->Go();
|
|
delete mg;
|
|
continue;
|
|
}
|
|
|
|
// gameState == 1: dispatch a la capa scenes::.
|
|
// El vell `ModuleSequence::Go()` feia aquest redirect al principi:
|
|
// 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;
|
|
}
|
|
|
|
auto scene = scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide);
|
|
if (!scene) {
|
|
// State no registrat — indica un bug del dispatcher o del
|
|
// registre d'escenes. Eixim ordenadament en lloc de cremar CPU.
|
|
break;
|
|
}
|
|
scene->onEnter();
|
|
Uint32 last = SDL_GetTicks();
|
|
while (!scene->done() && !JG_Quitting()) {
|
|
JI_Update(); // refresca key_pressed/any_key per a les escenes
|
|
const Uint32 now = SDL_GetTicks();
|
|
scene->tick(static_cast<int>(now - last));
|
|
last = now;
|
|
JD8_Flip(); // presenta i cedix al Director
|
|
}
|
|
gameState = scene->nextState();
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void Director::init() {
|
|
instance_ = new Director();
|
|
Gamepad::init();
|
|
|
|
// Registre d'escenes. Cada entrada = un state_key (`num_piramide`)
|
|
// amb una factory de `scenes::Scene`. El gameFiberEntry consulta
|
|
// aquest registry per a tots els states de seqüència; si una clau
|
|
// no apareix ací, el fiber eixirà del loop.
|
|
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).
|
|
for (int p = 2; p <= 5; ++p) {
|
|
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>(); });
|
|
// 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> {
|
|
if (Options::game.use_new_logo) {
|
|
return std::make_unique<scenes::IntroNewLogoScene>();
|
|
}
|
|
return std::make_unique<scenes::IntroScene>();
|
|
});
|
|
|
|
GameFiber::init(gameFiberEntry);
|
|
}
|
|
|
|
void Director::destroy() {
|
|
GameFiber::destroy();
|
|
Gamepad::destroy();
|
|
delete instance_;
|
|
instance_ = nullptr;
|
|
}
|
|
|
|
auto Director::get() -> Director* {
|
|
return instance_;
|
|
}
|
|
|
|
void Director::togglePause() {
|
|
paused_ = !paused_;
|
|
if (paused_) {
|
|
JA_PauseMusic();
|
|
} else {
|
|
JA_ResumeMusic();
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
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.
|
|
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()) {
|
|
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::run() {
|
|
setup();
|
|
while (true) {
|
|
pollAllEvents();
|
|
if (!iterate()) break;
|
|
}
|
|
teardown();
|
|
}
|
|
|
|
void Director::pollAllEvents() {
|
|
SDL_Event event;
|
|
while (SDL_PollEvent(&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();
|
|
}
|
|
|
|
auto Director::consumeKeyPressed() -> bool {
|
|
return key_pressed_.exchange(false);
|
|
}
|