Files
orni-attack/source/core/system/director.cpp
T
JailDesigner 5e82dc880f Plan A: frame loop al Director, interfaz Scene común
Cada escena ahora implementa una interfaz Scene { handleEvent, update,
draw, isFinished } y el Director es quien posee el bucle de frames.
Antes había tres bucles casi idénticos duplicados en logo/title/game
con ~30 LOC cada uno; ahora hay uno solo en Director::runFrameLoop.

Cambios:

- Nueva interfaz core/system/scene.hpp (pura virtual).
- Cada escena hereda final + override de handleEvent/update/draw/
  isFinished. Los métodos privados que ya existían (update, draw,
  processar_events) pasan a públicos como override; processar_events
  se renombra a handleEvent.
- Director::run gestiona la transición entre escenas vía
  buildScene(type) → runFrameLoop(scene), ambas estáticas.
- isFinished() = context_.nextScene() != mi_tipo, así que la única
  vía de transición es context_.setNextScene().
- TitleScene tenía un bug latente: llamaba a setNextScene(GAME)
  prematuramente al entrar en PLAYER_JOIN_PHASE, lo que con el nuevo
  modelo habría saltado las animaciones de salida. Movido el
  setNextScene al final de BLACK_SCREEN, que es donde la transición
  ocurría de verdad (vía la variable global SceneManager::actual).
- LogoScene::draw llamaba a clear() y present() dentro del draw, una
  anomalía. Sacados al Director para que la composición sea uniforme.
- Eliminadas todas las escrituras a SceneManager::actual desde las
  escenas. El Director es ahora la única fuente que actualiza la
  variable global (sigue ahí para lecturas externas, por compatibilidad).

Net: -60 LOC reales (las escenas pierden ~25 cada una de boilerplate),
y queda un único punto de inyección para los overlays globales que
vienen en el siguiente paso del roadmap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:25:56 +02:00

324 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "director.hpp"
#include <SDL3/SDL.h>
#include <sys/stat.h>
#include <algorithm>
#include <cerrno>
#include <cstdlib>
#include <iostream>
#include <memory>
#include "scene.hpp"
#include "scene_context.hpp"
#include "global_events.hpp"
#include "core/audio/audio.hpp"
#include "core/audio/audio_adapter.hpp"
#include "core/defaults.hpp"
#include "core/input/input.hpp"
#include "core/input/mouse.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/resources/resource_loader.hpp"
#include "core/utils/path_utils.hpp"
#include "game/scenes/game_scene.hpp"
#include "game/scenes/logo_scene.hpp"
#include "game/scenes/title_scene.hpp"
#include "game/options.hpp"
#include "project.h"
#ifndef _WIN32
#include <pwd.h>
#include <unistd.h>
#endif
// Using declarations per simplificar el codi
using SceneManager::SceneContext;
using SceneType = SceneContext::SceneType;
// Constructor
Director::Director(std::vector<std::string> const& args) {
std::cout << "Orni Attack - Inici\n";
// Inicialitzar opciones con valors per defecte
Options::init();
// Comprovar arguments del programa
executable_path_ = checkProgramArguments(args);
// Inicialitzar sistema de rutes
Utils::initializePathSystem(args[0].c_str());
// Obtenir ruta base dels recursos
std::string resource_base = Utils::getResourceBasePath();
// Inicialitzar sistema de recursos
#ifdef RELEASE_BUILD
// Mode release: paquet obligatori, sin fallback
std::string pack_path = resource_base + "/resources.pack";
if (!Resource::Helper::initializeResourceSystem(pack_path, false)) {
std::cerr << "ERROR FATAL: No es pot load " << pack_path << "\n";
std::cerr << "El juego no pot continuar sin los recursos.\n";
std::exit(1);
}
// Validar integritat del paquet
if (!Resource::Loader::get().validatePack()) {
std::cerr << "ERROR FATAL: El paquet de recursos está corromput\n";
std::exit(1);
}
std::cout << "Sistema de recursos inicialitzat (mode release)\n";
#else
// Mode desenvolupament: intentar paquet con fallback a data/
std::string pack_path = resource_base + "/resources.pack";
Resource::Helper::initializeResourceSystem(pack_path, true);
if (Resource::Helper::isPackLoaded()) {
std::cout << "Sistema de recursos inicialitzat (mode dev con paquet)\n";
} else {
std::cout << "Sistema de recursos inicialitzat (mode dev, fallback a data/)\n";
}
// Establir ruta base per al fallback
Resource::Loader::get().setBasePath(resource_base);
#endif
// Crear carpetes del sistema
createSystemFolder("jailgames");
createSystemFolder(std::string("jailgames/") + Project::NAME);
// Establir ruta del file de configuración
Options::setConfigFile(system_folder_ + "/config.yaml");
// Carregar o crear configuración
Options::loadFromFile();
// Inicialitzar sistema de input
Input::init("data/gamecontrollerdb.txt");
// Aplicar configuración de controls dels jugadors
Input::get()->applyPlayer1BindingsFromOptions();
Input::get()->applyPlayer2BindingsFromOptions();
if (Options::console) {
std::cout << "Configuración carregada\n";
std::cout << " Finestra: " << Options::window.width << "×"
<< Options::window.height << '\n';
std::cout << " Física: rotation=" << Options::physics.rotation_speed
<< " rad/s\n";
std::cout << " Input: " << Input::get()->getNumGamepads()
<< " gamepad(s) detectat(s)\n";
}
std::cout << '\n';
}
Director::~Director() {
// Guardar opciones
Options::saveToFile();
// Cleanup input
Input::destroy();
// Cleanup audio
Audio::destroy();
// Cleanup SDL
SDL_Quit();
std::cout << "\nAdéu!\n";
}
// Comprovar arguments del programa
auto Director::checkProgramArguments(std::vector<std::string> const& args)
-> std::string {
for (std::size_t i = 1; i < args.size(); ++i) {
const std::string& argument = args[i];
if (argument == "--console") {
Options::console = true;
std::cout << "Mode consola activat\n";
} else if (argument == "--reset-config") {
Options::init();
Options::saveToFile();
std::cout << "Configuración restablida als valors per defecte\n";
}
}
return args[0]; // Retornar ruta de l'executable
}
// Crear carpeta del sistema (específic per plataforma)
void Director::createSystemFolder(const std::string& folder) {
#ifdef _WIN32
system_folder_ = std::string(getenv("APPDATA")) + "/" + folder;
#elif __APPLE__
struct passwd* pw = getpwuid(getuid());
const char* homedir = pw->pw_dir;
system_folder_ =
std::string(homedir) + "/Library/Application Support/" + folder;
#elif __linux__
struct passwd* pw = getpwuid(getuid());
const char* homedir = pw->pw_dir;
system_folder_ = std::string(homedir) + "/.config/" + folder;
// CRÍTIC: Crear ~/.config si no existeix
{
std::string config_base_folder = std::string(homedir) + "/.config";
int ret = mkdir(config_base_folder.c_str(), S_IRWXU);
if (ret == -1 && errno != EEXIST) {
printf("ERROR: No es pot crear la carpeta ~/.config\n");
exit(EXIT_FAILURE);
}
}
#endif
// Comprovar si la carpeta existeix
struct stat st = {.st_dev = 0};
if (stat(system_folder_.c_str(), &st) == -1) {
errno = 0;
#ifdef _WIN32
int ret = mkdir(system_folder_.c_str());
#else
int ret = mkdir(system_folder_.c_str(), S_IRWXU);
#endif
if (ret == -1) {
switch (errno) {
case EACCES:
printf("ERROR: Permisos denegats creant %s\n", system_folder_.c_str());
exit(EXIT_FAILURE);
case EEXIST:
// La carpeta ya existeix (race condition), continuar
break;
case ENAMETOOLONG:
printf("ERROR: Ruta massa llarga: %s\n", system_folder_.c_str());
exit(EXIT_FAILURE);
default:
perror("mkdir");
exit(EXIT_FAILURE);
}
}
}
if (Options::console) {
std::cout << "Carpeta del sistema: " << system_folder_ << '\n';
}
}
// Bucle principal del juego
auto Director::run() -> int {
// Calculate initial size from saved zoom_factor
int initial_width = static_cast<int>(std::round(
Defaults::Window::WIDTH * Options::window.zoom_factor));
int initial_height = static_cast<int>(std::round(
Defaults::Window::HEIGHT * Options::window.zoom_factor));
// Crear gestor SDL con configuración de Options
SDLManager sdl(initial_width, initial_height, Options::window.fullscreen);
// CRÍTIC: Forçar ocultació del cursor DESPRÉS de toda la inicialización SDL
// Això evita que SDL mostre el cursor automàticament durante la creació de la finestra
if (!Options::window.fullscreen) {
Mouse::forceHide();
}
// Inicializar sistema de audio (config inyectada desde Defaults)
const Audio::Config AUDIO_CONFIG{
.enabled = Defaults::Audio::ENABLED,
.volume = Defaults::Audio::VOLUME,
.music_enabled = Defaults::Audio::MUSIC_ENABLED,
.music_volume = Defaults::Audio::MUSIC_VOLUME,
.sound_enabled = Defaults::Audio::SOUND_ENABLED,
.sound_volume = Defaults::Audio::SOUND_VOLUME,
};
Audio::init(AUDIO_CONFIG);
Audio::get()->applySettings(AUDIO_CONFIG); // Aplicar volúmenes iniciales al motor
// Precachear música para evitar lag al empezar
AudioResource::getMusic("title.ogg");
AudioResource::getMusic("game.ogg");
if (Options::console) {
std::cout << "Música precacheada\n";
}
// Crear context de escenes
SceneContext context;
#ifdef _DEBUG
context.setNextScene(SceneType::TITLE);
#else
context.setNextScene(SceneType::LOGO);
#endif
// Bucle principal: construir escena → frame loop → destruir → siguiente.
while (context.nextScene() != SceneType::EXIT) {
SceneManager::actual = context.nextScene();
std::unique_ptr<Scene> scene = buildScene(context.nextScene(), sdl, context);
if (!scene) {
break;
}
runFrameLoop(*scene, sdl, context);
}
SceneManager::actual = SceneType::EXIT;
return 0;
}
auto Director::buildScene(SceneType type, SDLManager& sdl, SceneContext& context)
-> std::unique_ptr<Scene> {
switch (type) {
case SceneType::LOGO:
return std::make_unique<LogoScene>(sdl, context);
case SceneType::TITLE:
return std::make_unique<TitleScene>(sdl, context);
case SceneType::GAME:
return std::make_unique<GameScene>(sdl, context);
case SceneType::EXIT:
default:
return nullptr;
}
}
void Director::runFrameLoop(Scene& scene, SDLManager& sdl, SceneContext& context) {
SDL_Event event;
Uint64 last_time = SDL_GetTicks();
while (!scene.isFinished()) {
// Delta time real, capeado a 50ms para evitar grandes saltos.
const Uint64 NOW = SDL_GetTicks();
float delta_time = static_cast<float>(NOW - last_time) / 1000.0F;
last_time = NOW;
delta_time = std::min(delta_time, 0.05F);
sdl.updateFPS(delta_time);
Mouse::updateCursorVisibility();
Input::get()->update();
// Event loop: primero ventana, después globales, después escena.
while (SDL_PollEvent(&event)) {
if (sdl.handleWindowEvent(event)) {
continue;
}
if (GlobalEvents::handle(event, sdl, context)) {
continue;
}
scene.handleEvent(event);
}
scene.update(delta_time);
Audio::update();
sdl.updateColors(delta_time); // no-op desde Fase 8c (oscilación en shader)
sdl.clear(0, 0, 0);
sdl.updateRenderingContext();
scene.draw();
// Hook futuro: overlays globales aquí (FPS+VSync, profilers...).
sdl.present();
}
}