diff --git a/CMakeLists.txt b/CMakeLists.txt index 21ceb87..5a345f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,7 @@ set(APP_SOURCES # --- core/system --- source/core/system/delta_time.cpp + source/core/system/demo.cpp source/core/system/director.cpp # --- game --- diff --git a/data/demo/demo.bin b/data/demo/demo.bin deleted file mode 100644 index cef4d56..0000000 Binary files a/data/demo/demo.bin and /dev/null differ diff --git a/data/demo/demo1.bin b/data/demo/demo1.bin new file mode 100644 index 0000000..b95466e Binary files /dev/null and b/data/demo/demo1.bin differ diff --git a/data/demo/demo2.bin b/data/demo/demo2.bin new file mode 100644 index 0000000..9844aaf Binary files /dev/null and b/data/demo/demo2.bin differ diff --git a/data/demo/demo3.bin b/data/demo/demo3.bin new file mode 100644 index 0000000..233aa92 Binary files /dev/null and b/data/demo/demo3.bin differ diff --git a/source/core/resources/resource.cpp b/source/core/resources/resource.cpp index 6f77bd8..afae406 100644 --- a/source/core/resources/resource.cpp +++ b/source/core/resources/resource.cpp @@ -144,8 +144,9 @@ void Resource::loadDataAsset(const std::string &bname, const std::vector 5 && bname.substr(0, 4) == "demo" && bname.substr(bname.size() - 4) == ".bin") { + // Acumula tots els demo*.bin (demo1.bin, demo2.bin, ...) en ordre d'aparicio + demo_bytes_.push_back(bytes); } // Menús (.men): se construyen en pass 2 porque dependen de textos y sonidos } diff --git a/source/core/resources/resource.h b/source/core/resources/resource.h index e41d428..5606789 100644 --- a/source/core/resources/resource.h +++ b/source/core/resources/resource.h @@ -29,7 +29,8 @@ class Resource { auto getAnimationLines(const std::string &name) -> std::vector &; auto getText(const std::string &name) -> Text *; // name sin extensión: "smb2", "nokia2", ... auto getMenu(const std::string &name) -> Menu *; // name sin extensión: "title", "options", ... - auto getDemoBytes() const -> const std::vector & { return demo_bytes_; } + [[nodiscard]] auto getDemoCount() const -> size_t { return demo_bytes_.size(); } + [[nodiscard]] auto getDemoBytes(size_t index) const -> const std::vector & { return demo_bytes_.at(index); } private: explicit Resource(SDL_Renderer *renderer); @@ -51,7 +52,7 @@ class Resource { std::unordered_map> animation_lines_; std::unordered_map texts_; std::unordered_map menus_; - std::vector demo_bytes_; + std::vector> demo_bytes_; static Resource *instance; }; diff --git a/source/core/system/demo.cpp b/source/core/system/demo.cpp new file mode 100644 index 0000000..0a8e45a --- /dev/null +++ b/source/core/system/demo.cpp @@ -0,0 +1,17 @@ +#include "core/system/demo.hpp" + +#include // for memcpy + +// Desempaqueta un blob binari amb TOTAL_DEMO_DATA registres consecutius +// de DemoKeys (struct POD de 6 bytes). Si el blob no te la mida esperada, +// torna un vector buit perque el playback el detecti i no peti. +auto loadDemoDataFromBytes(const std::vector &bytes) -> DemoData { + DemoData dd; + const size_t EXPECTED = sizeof(DemoKeys) * TOTAL_DEMO_DATA; + if (bytes.size() < EXPECTED) { + return dd; + } + dd.resize(TOTAL_DEMO_DATA); + std::memcpy(dd.data(), bytes.data(), EXPECTED); + return dd; +} diff --git a/source/core/system/demo.hpp b/source/core/system/demo.hpp new file mode 100644 index 0000000..d2975a0 --- /dev/null +++ b/source/core/system/demo.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include +#include +#include + +// Total de "frames" gravats a 60Hz de referencia. Equival a 2000/60 ~ 33.3s +// reals, independentment del refresc real perque el playback es time-based +// (index = elapsed_s * 60). +constexpr int TOTAL_DEMO_DATA = 2000; + +// Pulsacions per frame de referencia gravades a disc / reproduides al playback. +struct DemoKeys { + Uint8 left; + Uint8 right; + Uint8 no_input; + Uint8 fire; + Uint8 fire_left; + Uint8 fire_right; + + explicit DemoKeys(Uint8 l = 0, Uint8 r = 0, Uint8 ni = 0, Uint8 f = 0, Uint8 fl = 0, Uint8 fr = 0) + : left(l), + right(r), + no_input(ni), + fire(f), + fire_left(fl), + fire_right(fr) {} +}; + +// Una demo completa: vector de frames. +using DemoData = std::vector; + +// Estat del subsistema de demo dins de Game. +struct Demo { + bool enabled{false}; // Mode demo actiu (reproduccio) + bool recording{false}; // Mode gravacio actiu + float elapsed_s{0.0F}; // Temps acumulat des de l'inici de la demo + int index{0}; // index = elapsed_s * 60 (derivat) + DemoKeys keys; // Buffer de tecles del frame actual (gravacio) + std::vector data; // Vector de sets de demo carregats (multi-fitxer) +}; + +// Carrega un fitxer .bin (TOTAL_DEMO_DATA * sizeof(DemoKeys) bytes) i +// retorna el DemoData. Si el fitxer no es troba, retorna un DemoData buit. +auto loadDemoDataFromBytes(const std::vector &bytes) -> DemoData; diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 1b9ab73..d077248 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -338,7 +338,9 @@ auto Director::setFileList() -> bool { // Ficheros de configuración Asset::get()->add(system_folder_ + "/score.bin", Asset::Type::DATA, false, true); - Asset::get()->add(PREFIX + "/data/demo/demo.bin", Asset::Type::DATA); + Asset::get()->add(PREFIX + "/data/demo/demo1.bin", Asset::Type::DATA); + Asset::get()->add(PREFIX + "/data/demo/demo2.bin", Asset::Type::DATA); + Asset::get()->add(PREFIX + "/data/demo/demo3.bin", Asset::Type::DATA); // Musicas Asset::get()->add(PREFIX + "/data/music/intro.ogg", Asset::Type::MUSIC); diff --git a/source/game/game.cpp b/source/game/game.cpp index 95e9878..2d34ecb 100644 --- a/source/game/game.cpp +++ b/source/game/game.cpp @@ -277,7 +277,8 @@ void Game::init() { // Modo demo demo_.recording = false; - demo_.counter = 0; + demo_.elapsed_s = 0.0F; + demo_.index = 0; // Inicializa el objeto para el fundido fade_->init(0x27, 0x27, 0x36); @@ -521,34 +522,31 @@ auto Game::loadScoreFile() -> bool { return success; } -// Carga el fichero de datos para la demo +// Carga els fitxers de dades de demo (multi-set) des de Resource. Tots els +// blobs es descompacten a DemoData via loadDemoDataFromBytes; si un blob no +// te la mida esperada s'omet. auto Game::loadDemoFile() -> bool { - // Lee los datos de la demo desde Resource (precargados al arrancar). - const auto &bytes = Resource::get()->getDemoBytes(); - const size_t EXPECTED = sizeof(DemoKeys) * TOTAL_DEMO_DATA; - if (bytes.size() >= EXPECTED) { - for (int i = 0; i < TOTAL_DEMO_DATA; ++i) { - memcpy(&demo_.data_file[i], bytes.data() + (i * sizeof(DemoKeys)), sizeof(DemoKeys)); - } - if (Options::settings.console) { - std::cout << "Demo data loaded (" << bytes.size() << " bytes)" << '\n'; - } - } else { - // Si no hay datos (bytes vacíos o tamaño inválido), inicializamos a cero. - if (Options::settings.console) { - std::cout << "Warning: demo data missing or too small, initializing to zero" << '\n'; - } - for (auto &i : demo_.data_file) { - demo_.keys.left = 0; - demo_.keys.right = 0; - demo_.keys.no_input = 0; - demo_.keys.fire = 0; - demo_.keys.fire_left = 0; - demo_.keys.fire_right = 0; - i = demo_.keys; + demo_.data.clear(); + const size_t NUM = Resource::get()->getDemoCount(); + for (size_t i = 0; i < NUM; ++i) { + DemoData dd = loadDemoDataFromBytes(Resource::get()->getDemoBytes(i)); + if (!dd.empty()) { + demo_.data.push_back(std::move(dd)); } } - return true; + if (!demo_.data.empty()) { + demo_selected_set_ = static_cast(rand()) % demo_.data.size(); + } else { + demo_selected_set_ = 0; + } + if (Options::settings.console) { + if (demo_.data.empty()) { + std::cout << "Warning: no demo data loaded" << '\n'; + } else { + std::cout << "Demo data loaded (" << demo_.data.size() << " sets, playing #" << demo_selected_set_ << ")" << '\n'; + } + } + return !demo_.data.empty(); } // Guarda el fichero de puntos @@ -577,32 +575,29 @@ auto Game::saveScoreFile() -> bool { return success; } -// Guarda el fichero de datos para la demo +// Guarda el primer set de demo (gravat en mode RECORDING) a demo1.bin. auto Game::saveDemoFile() -> bool { - bool success = true; - const std::string P = Asset::get()->get("demo.bin"); - const std::string FILE_NAME = P.substr(P.find_last_of("\\/") + 1); - if (demo_.recording) { - SDL_IOStream *file = SDL_IOFromFile(P.c_str(), "w+b"); - if (file != nullptr) { - // Guardamos los datos - for (auto &i : demo_.data_file) { - SDL_WriteIO(file, &i, sizeof(DemoKeys)); - } - - if (Options::settings.console) { - std::cout << "Writing file " << FILE_NAME.c_str() << '\n'; - } - - // Cerramos el fichero - SDL_CloseIO(file); - } else { - if (Options::settings.console) { - std::cout << "Error: Unable to save " << FILE_NAME.c_str() << " file! " << SDL_GetError() << '\n'; - } - } + if (!demo_.recording || demo_.data.empty()) { + return true; } - return success; + const std::string P = Asset::get()->get("demo1.bin"); + const std::string FILE_NAME = P.substr(P.find_last_of("\\/") + 1); + SDL_IOStream *file = SDL_IOFromFile(P.c_str(), "w+b"); + if (file == nullptr) { + if (Options::settings.console) { + std::cout << "Error: Unable to save " << FILE_NAME << " file! " << SDL_GetError() << '\n'; + } + return false; + } + const auto &dd = demo_.data.at(0); + for (const auto &k : dd) { + SDL_WriteIO(file, &k, sizeof(DemoKeys)); + } + if (Options::settings.console) { + std::cout << "Writing file " << FILE_NAME << '\n'; + } + SDL_CloseIO(file); + return true; } // Inicializa las formaciones enemigas @@ -2276,6 +2271,12 @@ void Game::update(float dt_s) { elapsed_s_ += dt_s; counter_ = static_cast(elapsed_s_ * 60.0F); + // Avenc del temps de la demo (playback o gravacio). Index = elapsed_s * 60 + if (demo_.enabled || demo_.recording) { + demo_.elapsed_s += dt_s; + demo_.index = static_cast(demo_.elapsed_s * 60.0F); + } + checkGameInput(); updatePlayers(dt_s); updateBackground(dt_s); @@ -2446,10 +2447,33 @@ void Game::checkGameInput() { } } -// Rama de checkGameInput: reproduce el input grabado en data_file +// Rama de checkGameInput: reprodueix l'input gravat al set actiu de la demo. +// El index es time-based: index = elapsed_s * 60. L'avenc d'elapsed_s el fa +// Game::update() per evitar que el ritme de playback depengui dels frames +// que arribin a aquesta funcio. void Game::processDemoInput() { const int INDEX = 0; - const DemoKeys &keys = demo_.data_file[demo_.counter]; + + // Fi de la demo: salta a Title + if (demo_.index >= TOTAL_DEMO_DATA) { + section_->name = SECTION_PROG_TITLE; + section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS; + return; + } + + // Si no hi ha dades carregades, sortim al menu + if (demo_.data.empty()) { + section_->name = SECTION_PROG_TITLE; + return; + } + + // Accedeix al frame actual del set seleccionat amb % per seguretat + // davant de salts puntuals d'index. + const auto &dd = demo_.data.at(demo_selected_set_ % demo_.data.size()); + if (dd.empty()) { + return; + } + const DemoKeys &keys = dd.at(static_cast(demo_.index) % dd.size()); if (keys.left == 1) { players_[INDEX]->setInput(Input::Action::LEFT); @@ -2479,18 +2503,10 @@ void Game::processDemoInput() { players_[INDEX]->setFireCooldown(10); } - // Si se pulsa cualquier tecla, se sale del modo demo + // Si es prem qualsevol tecla, surt del mode demo if (Input::get()->checkAnyInput()) { section_->name = SECTION_PROG_TITLE; } - - // Incrementa el contador de la demo - if (demo_.counter < TOTAL_DEMO_DATA) { - demo_.counter++; - } else { - section_->name = SECTION_PROG_TITLE; - section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS; - } } // Rama de checkGameInput: lee inputs reales del teclado/gamepad por jugador @@ -2553,14 +2569,22 @@ void Game::processPlayerLiveInput(Player *player, int i) { section_->subsection = SUBSECTION_GAME_PAUSE; } - // Grabación de demo - if (demo_.counter < TOTAL_DEMO_DATA) { - if (demo_.recording) { - demo_.data_file[demo_.counter] = demo_.keys; + // Gravacio de demo (mode recording). L'index ja s'ha actualitzat a + // Game::update() via elapsed_s; aqui nomes escrivim el frame actual al + // primer set, redimensionant on demand. + if (demo_.recording) { + if (demo_.index >= TOTAL_DEMO_DATA) { + section_->name = SECTION_PROG_QUIT; + } else { + if (demo_.data.empty()) { + demo_.data.emplace_back(); + } + auto &dd = demo_.data.at(0); + if (dd.size() <= static_cast(demo_.index)) { + dd.resize(demo_.index + 1); + } + dd.at(demo_.index) = demo_.keys; } - demo_.counter++; - } else if (demo_.recording) { - section_->name = SECTION_PROG_QUIT; } } @@ -2590,7 +2614,7 @@ void Game::renderMessages() { // D E M O if (demo_.enabled) { - if (demo_.counter % 30 > 14) { + if (demo_.index % 30 > 14) { text_nokia_big2_->writeDX(Text::FLAG_CENTER, PLAY_AREA_CENTER_X, PLAY_AREA_FIRST_QUARTER_Y, Lang::get()->getText(37), 0, NO_COLOR, 2, SHADOW_COLOR); } } diff --git a/source/game/game.h b/source/game/game.h index 4b88249..c851df6 100644 --- a/source/game/game.h +++ b/source/game/game.h @@ -6,9 +6,10 @@ #include // for string, basic_string #include // for vector +#include "core/system/demo.hpp" // for Demo (estat de la demo) #include "game/entities/bullet.h" // for Bullet::Kind (signatura de createBullet) #include "game/entities/item.h" // for Item::Id (signatura de dropItem/createItem) -#include "utils/utils.h" // for DemoKeys, Color +#include "utils/utils.h" // for Color, Section class Balloon; class Fade; class Menu; @@ -45,7 +46,6 @@ class Game { // Cantidad de elementos a escribir en los ficheros de datos static constexpr int TOTAL_SCORE_DATA = 3; - static constexpr int TOTAL_DEMO_DATA = 2000; // Contadores static constexpr int STAGE_COUNTER = 200; @@ -138,14 +138,6 @@ class Game { int item_coffee_machine_odds; // Probabilidad de aparición del objeto }; - struct Demo { - bool enabled; // Indica si está activo el modo demo - bool recording; // Indica si está activado el modo para grabar la demo - Uint16 counter; // Contador para el modo demo - DemoKeys keys; // Variable con las pulsaciones de teclas del modo demo - DemoKeys data_file[TOTAL_DEMO_DATA]; // Datos del fichero con los movimientos para la demo - }; - void update(float dt_s); // Actualiza el juego void render(); // Dibuja el juego void init(); // Inicializa las variables necesarias para la sección 'Game' @@ -389,6 +381,7 @@ class Game { EnemyPool enemy_pool_[10]; // Variable con los diferentes conjuntos de formaciones enemigas Uint8 last_stage_reached_; // Contiene el numero de la última pantalla que se ha alcanzado Demo demo_; // Variable con todas las variables relacionadas con el modo demo + size_t demo_selected_set_{0}; // Index del set de demo a reproduir (escollit a loadDemoFile) int total_power_to_complete_game_; // La suma del poder necesario para completar todas las fases int clouds_speed_{0}; // Velocidad a la que se desplazan las nubes int pause_counter_; // Contador per a sortir del menu de pausa (frame-based, frames) diff --git a/source/game/scenes/title.cpp b/source/game/scenes/title.cpp index dd2f73a..b9f2911 100644 --- a/source/game/scenes/title.cpp +++ b/source/game/scenes/title.cpp @@ -853,6 +853,51 @@ void Title::applyOptions() { // Ejecuta un frame void Title::iterate() { + // Si el joc demo està actiu, NO consumim el dt aqui: el consumeix + // Game::iterate() en el seu propi tick(). Cridar-lo dues vegades per frame + // deixaria a Game un dt ~0 i la demo no avancaria (ni jugador ni globus). + if (demo_game_active_) { + // El demo Game necesita section->name == SECTION_PROG_GAME para funcionar + section_->name = SECTION_PROG_GAME; + demo_game_->iterate(); + + if (demo_game_->hasFinished()) { + // cppcheck-suppress knownConditionTrueFalse ; fals positiu: iterate() pot escriure section_->name=SECTION_PROG_QUIT (Alt+F4), cppcheck no creua la crida + const bool WAS_QUIT = (section_->name == SECTION_PROG_QUIT); + // Game::processDemoInput posa subsection=TITLE_INSTRUCTIONS només + // quan la demo s'acaba de manera natural (esgotat el playback). + // Si l'usuari l'ha saltat amb una tecla, la subsection queda en + // GAME_PLAY i tornem directament al titol, sense instructions. + const bool DEMO_ENDED_NATURALLY = (section_->subsection == SUBSECTION_TITLE_INSTRUCTIONS); + delete demo_game_; + demo_game_ = nullptr; + demo_game_active_ = false; + + // cppcheck-suppress knownConditionTrueFalse ; fals positiu: WAS_QUIT depèn de iterate() que pot mutar section_->name + if (WAS_QUIT) { + section_->name = SECTION_PROG_QUIT; + } else if (demo_then_instructions_ && DEMO_ENDED_NATURALLY) { + section_->name = SECTION_PROG_TITLE; + section_->subsection = SUBSECTION_TITLE_3; + runInstructions(Instructions::Mode::AUTO); + } else { + // Demo saltada: tornem a l'estat final del titol (TITLE_3, menu + // visible i musica) i reiniciem el comptador de demo perque no + // salti immediatament una altra vegada. + section_->name = SECTION_PROG_TITLE; + section_->subsection = SUBSECTION_TITLE_3; + demo_remaining_s_ = DEMO_TIMEOUT_S; + } + demo_then_instructions_ = false; + // Reset del rellotge per evitar un dt enorme al tornar al Title. + DeltaTime::reset(); + } else { + // Restaura section para que Director no transicione fuera de Title + section_->name = SECTION_PROG_TITLE; + } + return; + } + const float DELTA_TIME_S = DeltaTime::tick(); // Si las instrucciones están activas, delega el frame @@ -882,40 +927,6 @@ void Title::iterate() { return; } - // Si el juego demo está activo, delega el frame - if (demo_game_active_) { - // El demo Game necesita section->name == SECTION_PROG_GAME para funcionar - section_->name = SECTION_PROG_GAME; - demo_game_->iterate(); - - if (demo_game_->hasFinished()) { - // cppcheck-suppress knownConditionTrueFalse ; fals positiu: iterate() pot escriure section_->name=SECTION_PROG_QUIT (Alt+F4), cppcheck no creua la crida - const bool WAS_QUIT = (section_->name == SECTION_PROG_QUIT); - delete demo_game_; - demo_game_ = nullptr; - demo_game_active_ = false; - - // cppcheck-suppress knownConditionTrueFalse ; fals positiu: WAS_QUIT depèn de iterate() que pot mutar section_->name - if (WAS_QUIT) { - section_->name = SECTION_PROG_QUIT; - } else if (demo_then_instructions_) { - section_->name = SECTION_PROG_TITLE; - section_->subsection = SUBSECTION_TITLE_3; - demo_then_instructions_ = false; - runInstructions(Instructions::Mode::AUTO); - } else { - section_->name = SECTION_PROG_TITLE; - section_->subsection = SUBSECTION_TITLE_1; - } - // Reset del rellotge per evitar un dt enorme al tornar al Title. - DeltaTime::reset(); - } else { - // Restaura section para que Director no transicione fuera de Title - section_->name = SECTION_PROG_TITLE; - } - return; - } - // Ejecución normal del título update(DELTA_TIME_S); render(); diff --git a/source/utils/utils.h b/source/utils/utils.h index 5f43eac..4510057 100644 --- a/source/utils/utils.h +++ b/source/utils/utils.h @@ -60,21 +60,11 @@ struct Section { Uint8 subsection; }; -// Estructura para mapear el teclado usado en la demo -struct DemoKeys { - Uint8 left; - Uint8 right; - Uint8 no_input; - Uint8 fire; - Uint8 fire_left; - Uint8 fire_right; -}; - // Estructura para albergar métodos de control struct InputDevice { - int id; // Identificador en el vector de mandos - std::string name; // Nombre del dispositivo - Input::Device device_type; // Tipo de dispositivo (teclado o mando) + int id; // Identificador en el vector de mandos + std::string name; // Nombre del dispositivo + Input::Device device_type; // Tipo de dispositivo (teclado o mando) }; // Calcula el cuadrado de la distancia entre dos puntos