5 singletons afectats: Audio, Screen, Director, Resource::Cache, Resource::List. - static T* instance → static std::unique_ptr<T> instance - init(): new T() adoptat immediatament per unique_ptr (ownership RAII) - destroy(): instance.reset() (sense delete manual) - get(): retorna instance.get() - Destructors moguts a public perquè std::default_delete hi pugui accedir (ctors privats + copy/move deleted → encapsulació efectiva mantinguda) Ordre de destrucció preservat: SDL_AppQuit segueix cridant destroy() en l'ordre invers a init() — la RAII automàtica no s'activa fins al final del programa (LIFO de variables static). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
409 lines
16 KiB
C++
409 lines
16 KiB
C++
#include "core/system/director.hpp"
|
|
|
|
#include <cstring>
|
|
#include <iostream>
|
|
|
|
#include "core/audio/audio.hpp"
|
|
#include "core/input/gamepad.hpp"
|
|
#include "core/input/global_inputs.hpp"
|
|
#include "core/input/key_config.hpp"
|
|
#include "core/input/key_remap.hpp"
|
|
#include "core/input/mouse.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/resources/resource_cache.hpp"
|
|
#include "game/info.hpp"
|
|
#include "game/modulegame.hpp"
|
|
#include "game/options.hpp"
|
|
#include "scenes/banner_scene.hpp"
|
|
#include "scenes/boot_loader_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);
|
|
|
|
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;
|
|
|
|
FILE* ini = fopen("trick.ini", "rb");
|
|
if (ini != nullptr) {
|
|
info::ctx.nou_personatge = true;
|
|
fclose(ini);
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<scenes::Scene> Director::createNextScene() {
|
|
// 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>();
|
|
}
|
|
if (game_state_ == 0) {
|
|
// 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;
|
|
}
|
|
return scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide);
|
|
}
|
|
|
|
void Director::init() {
|
|
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
|
|
// 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>(); });
|
|
// 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>();
|
|
});
|
|
}
|
|
|
|
void Director::destroy() {
|
|
Gamepad::destroy();
|
|
instance_.reset();
|
|
}
|
|
|
|
auto Director::get() -> Director* {
|
|
return instance_.get();
|
|
}
|
|
|
|
void Director::togglePause() {
|
|
paused_ = !paused_;
|
|
if (paused_) {
|
|
Audio::get()->pauseMusic();
|
|
} else {
|
|
Audio::get()->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 (quit_requested_) {
|
|
JG_QuitSignal();
|
|
current_scene_.reset(); // destrueix l'escena actual ordenadament
|
|
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
|
|
}
|
|
|
|
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();
|
|
|
|
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;
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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();
|
|
current_scene_.reset();
|
|
}
|
|
|
|
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 (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;
|
|
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ó.
|
|
// 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;
|
|
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 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);
|
|
}
|
|
}
|
|
}
|
|
Mouse::handleEvent(event);
|
|
}
|
|
|
|
void Director::requestQuit() {
|
|
quit_requested_ = true;
|
|
JG_QuitSignal();
|
|
}
|
|
|
|
void Director::requestRestart() {
|
|
restart_requested_ = true;
|
|
}
|
|
|
|
auto Director::consumeKeyPressed() -> bool {
|
|
return key_pressed_.exchange(false);
|
|
}
|