Files
coffee-crisis/source/game/scenes/title.cpp
T

1120 lines
39 KiB
C++

#include "game/scenes/title.h"
#include <SDL3/SDL.h>
#include <algorithm> // for min
#include <cstdlib> // for rand
#include <iostream> // for basic_ostream, operator<<, basic_ostrea...
#include <string> // for basic_string, operator+, char_traits
#include "core/audio/audio.hpp" // for Audio
#include "core/input/global_inputs.hpp" // for GlobalInputs::handle
#include "core/input/input.h" // for Input, Input::Device::GAMECONTROLLER, INPUT_...
#include "core/locale/lang.h" // for Lang, Lang::Code
#include "core/rendering/animatedsprite.h" // for AnimatedSprite
#include "core/rendering/fade.h" // for Fade
#include "core/rendering/screen.h" // for Screen
#include "core/rendering/smartsprite.h" // for SmartSprite
#include "core/rendering/sprite.h" // for Sprite
#include "core/rendering/text.h" // for Text
#include "core/rendering/texture.h" // for Texture
#include "core/resources/asset.h" // for Asset
#include "core/resources/resource.h"
#include "core/system/delta_time.hpp" // for DeltaTime::reset / tick
#include "game/defaults.hpp" // for GAMECANVAS_CENTER_X, SECTION_PROG_QUIT
#include "game/game.h" // for Game
#include "game/options.hpp" // for Options
#include "game/ui/menu.h" // for Menu
// Constructor
Title::Title(SDL_Renderer *renderer, Section *section) {
// Copia las direcciones de los punteros
this->renderer_ = renderer;
this->section_ = section;
// Reserva memoria para los punteros
event_handler_ = new SDL_Event();
fade_ = new Fade(renderer);
Resource *resource = Resource::get();
dust_texture_ = resource->getTexture("title_dust.png");
coffee_texture_ = resource->getTexture("title_coffee.png");
crisis_texture_ = resource->getTexture("title_crisis.png");
gradient_texture_ = resource->getTexture("title_gradient.png");
coffee_bitmap_ = new SmartSprite(coffee_texture_, renderer);
crisis_bitmap_ = new SmartSprite(crisis_texture_, renderer);
dust_bitmap_left_ = new AnimatedSprite(dust_texture_, renderer, "", &resource->getAnimationLines("title_dust.ani"));
dust_bitmap_right_ = new AnimatedSprite(dust_texture_, renderer, "", &resource->getAnimationLines("title_dust.ani"));
gradient_ = new Sprite({0, 0, 256, 192}, gradient_texture_, renderer);
text1_ = resource->getText("smb2");
text2_ = resource->getText("8bithud");
#ifdef GAME_CONSOLE
menu_.title = R->getMenu("title_gc");
menu_.options = R->getMenu("options_gc");
#else
menu_.title = resource->getMenu("title");
menu_.options = resource->getMenu("options");
#endif
menu_.player_select = resource->getMenu("player_select");
#ifdef __EMSCRIPTEN__
// En la versión web no se puede cerrar el programa: ocultamos la opción QUIT del menú de título
menu_.title->setVisible(3, false);
menu_.title->setSelectable(3, false);
#endif
// Sonidos y música (handles compartidos)
crash_sound_ = resource->getSound("title.wav");
title_music_ = resource->getMusic("title.ogg");
// Inicializa los valores
init();
// Reset del rellotge: la primera crida a tick() retornarà ~0.
DeltaTime::reset();
}
// Destructor
Title::~Title() {
delete event_handler_;
delete fade_;
// Las texturas, Text, Menu, sonido y música son propiedad de Resource —
// no se liberan aquí. Solo los sprites que posee esta escena.
delete coffee_bitmap_;
delete crisis_bitmap_;
delete dust_bitmap_left_;
delete dust_bitmap_right_;
delete gradient_;
SDL_DestroyTexture(background_);
}
// Inicializa los valores
void Title::init() {
// Inicializa variables
section_->subsection = SUBSECTION_TITLE_1;
demo_remaining_s_ = DEMO_TIMEOUT_S;
bg_scroll_x_s_ = 0.0F;
bg_scroll_y_s_ = 0.0F;
bg_phase_s_ = 0.0F;
blink_phase_s_ = 0.0F;
background_mode_ = rand() % 2;
menu_visible_ = false;
menu_.active = menu_.title;
next_section_.name = SECTION_PROG_GAME;
post_fade_ = 0;
fade_->init(0x17, 0x17, 0x26);
demo_ = true;
vibration_elapsed_s_ = 0.0F;
vibration_initialized_ = false;
instructions_active_ = false;
demo_game_active_ = false;
demo_then_instructions_ = false;
// Pone valores por defecto a las opciones de control
Options::inputs.clear();
InputDevice inp;
inp.id = 0;
inp.name = "KEYBOARD";
inp.device_type = Input::Device::KEYBOARD;
Options::inputs.push_back(inp);
inp.id = 0;
inp.name = "GAME CONTROLLER";
inp.device_type = Input::Device::GAMECONTROLLER;
Options::inputs.push_back(inp);
// Comprueba si hay mandos conectados
checkInputDevices();
// Pone valores por defecto. El primer jugador el teclado. El segundo jugador el primer mando
device_index_.clear();
device_index_.push_back(available_input_devices_.size() - 1); // El último dispositivo encontrado es el teclado
device_index_.push_back(0); // El primer mando encontrado. Si no ha encontrado ninguno es el teclado
// Si ha encontrado un mando se lo asigna al segundo jugador
if (Input::get()->gameControllerFound()) {
Options::inputs[1].id = available_input_devices_[device_index_[1]].id;
Options::inputs[1].name = available_input_devices_[device_index_[1]].name;
Options::inputs[1].device_type = available_input_devices_[device_index_[1]].device_type;
} else { // Si no ha encontrado un mando, deshabilita la opción de jugar a 2 jugadores
menu_.title->setSelectable(1, false);
menu_.title->setGreyed(1, true);
}
// Inicializa el bitmap de Coffee
coffee_bitmap_->init();
coffee_bitmap_->setPosX(45);
coffee_bitmap_->setPosY(11 - 200);
coffee_bitmap_->setWidth(167);
coffee_bitmap_->setHeight(46);
coffee_bitmap_->setVelX(0.0F);
coffee_bitmap_->setVelY(150.0F); // 2.5 px/tick ⇒ 150 px/s
coffee_bitmap_->setAccelX(0.0F);
coffee_bitmap_->setAccelY(360.0F); // 0.1 px/tick² ⇒ 360 px/s²
coffee_bitmap_->setSpriteClip(0, 0, 167, 46);
coffee_bitmap_->setEnabled(true);
coffee_bitmap_->setRemainingTime(0.0F);
coffee_bitmap_->setDestX(45);
coffee_bitmap_->setDestY(11);
// Inicializa el bitmap de Crisis
crisis_bitmap_->init();
crisis_bitmap_->setPosX(60);
crisis_bitmap_->setPosY(57 + 200);
crisis_bitmap_->setWidth(137);
crisis_bitmap_->setHeight(46);
crisis_bitmap_->setVelX(0.0F);
crisis_bitmap_->setVelY(-150.0F); // -2.5 px/tick ⇒ -150 px/s
crisis_bitmap_->setAccelX(0.0F);
crisis_bitmap_->setAccelY(-360.0F); // -0.1 px/tick² ⇒ -360 px/s²
crisis_bitmap_->setSpriteClip(0, 0, 137, 46);
crisis_bitmap_->setEnabled(true);
crisis_bitmap_->setRemainingTime(0.0F);
crisis_bitmap_->setDestX(60);
crisis_bitmap_->setDestY(57);
// Inicializa el bitmap de DustRight
dust_bitmap_right_->resetAnimation();
dust_bitmap_right_->setPosX(218);
dust_bitmap_right_->setPosY(47);
dust_bitmap_right_->setWidth(16);
dust_bitmap_right_->setHeight(16);
dust_bitmap_right_->setFlip(SDL_FLIP_HORIZONTAL);
// Inicializa el bitmap de DustLeft
dust_bitmap_left_->resetAnimation();
dust_bitmap_left_->setPosX(33);
dust_bitmap_left_->setPosY(47);
dust_bitmap_left_->setWidth(16);
dust_bitmap_left_->setHeight(16);
// Inicializa el sprite con el degradado
gradient_->setSpriteClip(0, 96, 256, 192);
// Crea el mosaico de fondo del titulo
createTiledBackground();
// Coloca la ventana que recorre el mosaico de fondo de manera que coincida con el mosaico que hay pintado en el titulo al iniciar
background_window_.x = 128;
background_window_.y = 96;
background_window_.w = GAMECANVAS_WIDTH;
background_window_.h = GAMECANVAS_HEIGHT;
// Inicializa los valores del vector con los valores del seno
for (int i = 0; i < 360; ++i) {
sin_[i] = SDL_sinf((float)i * 3.14F / 180.0F);
}
// Actualiza los textos de los menus
updateMenuLabels();
}
// Actualiza las variables del objeto (time-based)
void Title::update(float dt_s) {
Audio::update();
checkInput();
switch (section_->subsection) {
case SUBSECTION_TITLE_1:
updateTitle1(dt_s);
break;
case SUBSECTION_TITLE_2:
updateTitle2(dt_s);
break;
case SUBSECTION_TITLE_3:
updateTitle3(dt_s);
break;
default:
break;
}
}
// Sección 1 - Titulo desplazandose
void Title::updateTitle1(float dt_s) {
coffee_bitmap_->update(dt_s);
crisis_bitmap_->update(dt_s);
// Si los objetos han llegado a su destino, cambiamos de Sección
if (coffee_bitmap_->hasFinished() && crisis_bitmap_->hasFinished()) {
section_->subsection = SUBSECTION_TITLE_2;
// Pantallazo blanco: pintar sobre el gameCanvas y dejar
// que Screen::blit() presente por la ruta activa (GPU o
// SDL_Renderer). Un `SDL_RenderPresent(renderer)` directe
// crasheja quan el SDL3 GPU ha reclamat la ventana.
Screen::get()->start();
SDL_SetRenderDrawColor(renderer_, 0xFF, 0xFF, 0xFF, 0xFF);
SDL_RenderClear(renderer_);
Screen::get()->blit();
Audio::get()->playSound(crash_sound_);
}
}
// Sección 2 - Titulo vibrando
void Title::updateTitle2(float dt_s) {
// Captura las posiciones base la primera vez
if (!vibration_initialized_) {
vibration_coffee_base_x_ = coffee_bitmap_->getPosX();
vibration_crisis_base_x_ = crisis_bitmap_->getPosX();
vibration_initialized_ = true;
}
// Patró d'offset horitzontal (15 valors, una vegada cadascun cada 3 frames)
const int V[] = {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, 0};
constexpr int V_SIZE = static_cast<int>(sizeof(V) / sizeof(V[0]));
const int IDX = std::min(static_cast<int>(vibration_elapsed_s_ / VIBRATION_STEP_DURATION_S), V_SIZE - 1);
coffee_bitmap_->setPosX(vibration_coffee_base_x_ + V[IDX]);
crisis_bitmap_->setPosX(vibration_crisis_base_x_ + V[IDX]);
dust_bitmap_right_->update(dt_s);
dust_bitmap_left_->update(dt_s);
vibration_elapsed_s_ += dt_s;
if (vibration_elapsed_s_ >= VIBRATION_DURATION_S) {
section_->subsection = SUBSECTION_TITLE_3;
vibration_elapsed_s_ = 0.0F;
vibration_initialized_ = false;
}
}
// Sección 3 - La pantalla de titulo con el menú y la música
void Title::updateTitle3(float dt_s) {
if (demo_remaining_s_ > 0.0F) {
if (Audio::getRealMusicState() == Audio::MusicState::STOPPED) {
Audio::get()->playMusic(title_music_);
}
dust_bitmap_right_->update(dt_s);
dust_bitmap_left_->update(dt_s);
fade_->update(dt_s);
if (fade_->hasEnded()) {
handlePostFadeAction();
}
updateBG(dt_s);
blink_phase_s_ += dt_s;
if (blink_phase_s_ >= PRESS_ANY_KEY_PERIOD_S) {
blink_phase_s_ -= PRESS_ANY_KEY_PERIOD_S;
}
if (menu_visible_ && !fade_->isEnabled()) {
menu_.active->update();
}
if (menu_.active->getName() == "TITLE") {
handleTitleMenuSelection();
}
if (menu_.active->getName() == "PLAYER_SELECT") {
handlePlayerSelectMenuSelection();
}
if (menu_.active->getName() == "OPTIONS") {
handleOptionsMenuSelection();
}
if (menu_.active->getName() == "TITLE") {
demo_remaining_s_ -= dt_s;
}
} else {
if (demo_) {
demo_then_instructions_ = true;
runDemoGame();
} else {
section_->name = SECTION_PROG_LOGO;
}
}
if (section_->subsection == SUBSECTION_TITLE_INSTRUCTIONS) {
runInstructions(Instructions::Mode::AUTO);
}
}
// Acción tras finalizar el fundido del título
void Title::handlePostFadeAction() {
switch (post_fade_) {
case 0: // 1 PLAYER
section_->name = SECTION_PROG_GAME;
section_->subsection = SUBSECTION_GAME_PLAY_1P;
Audio::get()->stopMusic();
break;
case 1: // 2 PLAYERS
section_->name = SECTION_PROG_GAME;
section_->subsection = SUBSECTION_GAME_PLAY_2P;
Audio::get()->stopMusic();
break;
case 2: // QUIT
#ifndef __EMSCRIPTEN__
section_->name = SECTION_PROG_QUIT;
Audio::get()->stopMusic();
#endif
break;
case 3: // TIME OUT
demo_remaining_s_ = DEMO_TIMEOUT_S;
menu_.active->reset();
if (demo_) {
demo_then_instructions_ = true;
runDemoGame();
} else {
section_->name = SECTION_PROG_LOGO;
}
break;
default:
break;
}
}
// Procesa selección del menú TITLE
void Title::handleTitleMenuSelection() {
switch (menu_.active->getItemSelected()) {
case 0: // 1 PLAYER -> Cambia al manu de selección de jugador
menu_.active = menu_.player_select;
break;
case 1: // 2 PLAYERS
post_fade_ = 1;
fade_->activateFade();
break;
case 2: // OPTIONS
menu_.active = menu_.options;
prev_video_ = Options::video;
prev_window_ = Options::window;
prev_settings_ = Options::settings;
prev_inputs_ = Options::inputs;
break;
case 3: // QUIT
post_fade_ = 2;
fade_->activateFade();
break;
default:
break;
}
}
// Procesa selección del menú PLAYER_SELECT
void Title::handlePlayerSelectMenuSelection() {
switch (menu_.active->getItemSelected()) {
case 0:
// Este item no se puede seleccionar y actua de titulo
break;
case 1: // BAL1
post_fade_ = 0;
Options::settings.player_selected = 0;
fade_->activateFade();
break;
case 2: // AROUNDER
post_fade_ = 0;
Options::settings.player_selected = 1;
fade_->activateFade();
break;
case 3: // BACK
menu_.active = menu_.title;
menu_.player_select->reset();
break;
default:
break;
}
}
// Procesa selección del menú OPTIONS
void Title::handleOptionsMenuSelection() {
switch (menu_.active->getItemSelected()) {
case 0: // Difficulty
if (Options::settings.difficulty == DIFFICULTY_EASY) {
Options::settings.difficulty = DIFFICULTY_NORMAL;
} else if (Options::settings.difficulty == DIFFICULTY_NORMAL) {
Options::settings.difficulty = DIFFICULTY_HARD;
} else {
Options::settings.difficulty = DIFFICULTY_EASY;
}
updateMenuLabels();
break;
case 1: // PLAYER 1 CONTROLS
updatePlayerInputs(0);
updateMenuLabels();
break;
case 3: // PLAYER 2 CONTROLS
updatePlayerInputs(1);
updateMenuLabels();
break;
case 5: // Language
Options::settings.language = Lang::nextLanguage(Options::settings.language);
updateMenuLabels();
break;
case 6: // Display mode
switchFullScreenModeVar();
if (Options::video.fullscreen) {
menu_.options->setSelectable(8, false);
menu_.options->setGreyed(8, true);
} else {
menu_.options->setSelectable(8, true);
menu_.options->setGreyed(8, false);
}
updateMenuLabels();
break;
case 8: // Windows size
Options::window.zoom++;
if (Options::window.zoom > Options::window.max_zoom) {
Options::window.zoom = 1;
}
updateMenuLabels();
break;
case 9: // Scale mode
Options::video.scale_mode = (Options::video.scale_mode == SDL_SCALEMODE_NEAREST)
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
Texture::setGlobalScaleMode(Options::video.scale_mode);
reLoadTextures();
updateMenuLabels();
break;
case 10: // VSYNC
Options::video.vsync = !Options::video.vsync;
updateMenuLabels();
break;
case 11: // HOW TO PLAY
runInstructions(Instructions::Mode::MANUAL);
break;
case 12: // ACCEPT
applyOptions();
menu_.active->reset();
menu_.active = menu_.title;
break;
case 13: // CANCEL
Options::video = prev_video_;
Options::window = prev_window_;
Options::settings = prev_settings_;
Options::inputs = prev_inputs_;
updateMenuLabels();
menu_.active->reset();
menu_.active = menu_.title;
break;
default:
break;
}
}
// Dibuja el objeto en pantalla
void Title::render() {
switch (section_->subsection) {
// Sección 1 - Titulo desplazandose
case SUBSECTION_TITLE_1: {
// Prepara para empezar a dibujar en la textura de juego
Screen::get()->start();
// Limpia la pantalla
Screen::get()->clean(BG_COLOR);
// Dibuja el tileado de fondo
{
SDL_FRect f_src = {(float)background_window_.x, (float)background_window_.y, (float)background_window_.w, (float)background_window_.h};
SDL_RenderTexture(renderer_, background_, &f_src, nullptr);
};
// Dibuja el degradado
gradient_->render();
// Dibuja los objetos
coffee_bitmap_->render();
crisis_bitmap_->render();
// Vuelca el contenido del renderizador en pantalla
Screen::get()->blit();
} break;
// Sección 2 - Titulo vibrando
case SUBSECTION_TITLE_2: {
// Prepara para empezar a dibujar en la textura de juego
Screen::get()->start();
// Limpia la pantalla
Screen::get()->clean(BG_COLOR);
// Dibuja el tileado de fondo
{
SDL_FRect f_src = {(float)background_window_.x, (float)background_window_.y, (float)background_window_.w, (float)background_window_.h};
SDL_RenderTexture(renderer_, background_, &f_src, nullptr);
};
// Dibuja el degradado
gradient_->render();
// Dibuja los objetos (posiciones ya actualizadas por update)
coffee_bitmap_->render();
crisis_bitmap_->render();
dust_bitmap_right_->render();
dust_bitmap_left_->render();
// Vuelca el contenido del renderizador en pantalla
Screen::get()->blit();
} break;
// Sección 3 - La pantalla de titulo con el menú y la música
case SUBSECTION_TITLE_3: { // Prepara para empezar a dibujar en la textura de juego
Screen::get()->start();
// Limpia la pantalla
Screen::get()->clean(BG_COLOR);
// Dibuja el tileado de fondo
{
SDL_FRect f_src = {(float)background_window_.x, (float)background_window_.y, (float)background_window_.w, (float)background_window_.h};
SDL_RenderTexture(renderer_, background_, &f_src, nullptr);
};
// Dibuja el degradado
gradient_->render();
// Dibuja los objetos
if (menu_.active->getName() != "OPTIONS") {
// Bitmaps con el logo/titulo del juego
coffee_bitmap_->render();
crisis_bitmap_->render();
// Texto con el copyright y versión
text2_->writeDX(Text::FLAG_CENTER | Text::FLAG_SHADOW, GAMECANVAS_CENTER_X, GAMECANVAS_HEIGHT - (BLOCK * 2), COPYRIGHT, 1, NO_COLOR, 1, SHADOW_COLOR);
}
if (menu_visible_) {
menu_.active->render();
}
dust_bitmap_right_->render();
dust_bitmap_left_->render();
// PRESS ANY KEY! Blink: visible quan la fase passa l'umbral "off".
if ((blink_phase_s_ > PRESS_ANY_KEY_OFF_S) && (!menu_visible_)) {
text1_->writeDX(Text::FLAG_CENTER | Text::FLAG_SHADOW, GAMECANVAS_CENTER_X, PLAY_AREA_THIRD_QUARTER_Y + BLOCK, Lang::get()->getText(23), 1, NO_COLOR, 1, SHADOW_COLOR);
}
// Fade
fade_->render();
// Vuelca el contenido del renderizador en pantalla
Screen::get()->blit();
} break;
default:
break;
}
}
// Comprueba las entradas. ESC (Action::EXIT) ja el gestiona
// GlobalInputs::handle() amb doble pulsació; el quit es propaga via
// Director::iterate.
void Title::checkInput() {
GlobalInputs::handle();
}
// Actualiza el tileado de fondo (time-based).
void Title::updateBG(float dt_s) {
if (background_mode_ == 0) {
// Diagonal: 60 px/s a 60Hz ⇒ un cicle de 64 px cada 64/60 = 1.067 s.
// Ancorat a la posició inicial (128, 96): t=0 dona la mateixa vista que
// l'estàtic dels Title1/Title2, sense salt visual a l'entrada del Title3.
constexpr float SCROLL_PERIOD_S = 64.0F / BG_SCROLL_SPEED_PX_PER_S;
bg_scroll_x_s_ += dt_s;
bg_scroll_y_s_ += dt_s;
if (bg_scroll_x_s_ >= SCROLL_PERIOD_S) { bg_scroll_x_s_ -= SCROLL_PERIOD_S; }
if (bg_scroll_y_s_ >= SCROLL_PERIOD_S) { bg_scroll_y_s_ -= SCROLL_PERIOD_S; }
background_window_.x = 128 + static_cast<int>(bg_scroll_x_s_ * BG_SCROLL_SPEED_PX_PER_S);
background_window_.y = 96 + static_cast<int>(bg_scroll_y_s_ * BG_SCROLL_SPEED_PX_PER_S);
} else {
// Cercle: 360 graus en BG_CIRCLE_PERIOD_S segons.
bg_phase_s_ += dt_s;
if (bg_phase_s_ >= BG_CIRCLE_PERIOD_S) { bg_phase_s_ -= BG_CIRCLE_PERIOD_S; }
const int ANGLE = static_cast<int>((bg_phase_s_ / BG_CIRCLE_PERIOD_S) * 360.0F) % 360;
background_window_.x = 128 + (int(sin_[(ANGLE + 270) % 360] * 128));
background_window_.y = 96 + (int(sin_[(360 - ANGLE) % 360] * 96));
}
}
// Cambia el valor de la variable de modo de pantalla completa
void Title::switchFullScreenModeVar() {
Options::video.fullscreen = !Options::video.fullscreen;
}
// Actualiza los elementos de los menus
void Title::updateMenuLabels() const {
int i = 0;
// DIFFICULTY
switch (Options::settings.difficulty) {
case DIFFICULTY_EASY:
menu_.options->setItemCaption(i, Lang::get()->getText(59) + ": " + Lang::get()->getText(66)); // EASY
break;
case DIFFICULTY_NORMAL:
menu_.options->setItemCaption(i, Lang::get()->getText(59) + ": " + Lang::get()->getText(67)); // NORMAL
break;
case DIFFICULTY_HARD:
menu_.options->setItemCaption(i, Lang::get()->getText(59) + ": " + Lang::get()->getText(68)); // HARD
break;
default:
menu_.options->setItemCaption(i, Lang::get()->getText(59) + ": " + Lang::get()->getText(67)); // NORMAL
break;
}
i++;
// PLAYER 1 CONTROLS
menu_.options->setItemCaption(i, Lang::get()->getText(62));
i++;
// PLAYER 1 CONTROLS - OPTIONS
switch (Options::inputs[0].device_type) {
case Input::Device::KEYBOARD:
menu_.options->setItemCaption(i, Lang::get()->getText(69)); // KEYBOARD
menu_.options->setGreyed(i, false);
break;
case Input::Device::GAMECONTROLLER:
menu_.options->setItemCaption(i, Lang::get()->getText(70)); // GAME CONTROLLER
if (!Input::get()->gameControllerFound()) {
menu_.options->setGreyed(i, true);
} else {
menu_.options->setGreyed(i, false);
menu_.options->setItemCaption(i, Options::inputs[0].name);
}
break;
default:
menu_.options->setItemCaption(i, Lang::get()->getText(69)); // KEYBOARD
break;
}
i++;
// PLAYER 2 CONTROLS
menu_.options->setItemCaption(i, Lang::get()->getText(63));
i++;
// PLAYER 2 CONTROLS - OPTIONS
switch (Options::inputs[1].device_type) {
case Input::Device::KEYBOARD:
menu_.options->setItemCaption(i, Lang::get()->getText(69)); // KEYBOARD
menu_.options->setGreyed(i, false);
break;
case Input::Device::GAMECONTROLLER:
menu_.options->setItemCaption(i, Lang::get()->getText(70)); // GAME CONTROLLER
if (!Input::get()->gameControllerFound()) {
menu_.options->setGreyed(i, true);
} else {
menu_.options->setGreyed(i, false);
menu_.options->setItemCaption(i, Options::inputs[1].name);
}
break;
default:
menu_.options->setItemCaption(i, Lang::get()->getText(69)); // KEYBOARD
break;
}
i++;
// LANGUAGE
switch (Options::settings.language) {
case Lang::Code::ES_ES:
menu_.options->setItemCaption(i, Lang::get()->getText(8) + ": " + Lang::get()->getText(24)); // SPANISH
break;
case Lang::Code::BA_BA:
menu_.options->setItemCaption(i, Lang::get()->getText(8) + ": " + Lang::get()->getText(25)); // VALENCIAN
break;
case Lang::Code::EN_UK:
default:
// ENGLISH (i fallback per a llengües no reconegudes)
menu_.options->setItemCaption(i, Lang::get()->getText(8) + ": " + Lang::get()->getText(26));
break;
}
i++;
// DISPLAY MODE
menu_.options->setItemCaption(i, Lang::get()->getText(58));
i++;
// DISPLAY MODE - OPTIONS
menu_.options->setItemCaption(i, Options::video.fullscreen ? Lang::get()->getText(5) : Lang::get()->getText(4));
i++;
// WINDOW SIZE
menu_.options->setItemCaption(i, Lang::get()->getText(7) + " x" + std::to_string(Options::window.zoom)); // WINDOW SIZE
i++;
// SCALE MODE
if (Options::video.scale_mode == SDL_SCALEMODE_LINEAR) {
menu_.options->setItemCaption(i, Lang::get()->getText(60) + ": " + Lang::get()->getText(71)); // BILINEAL
} else {
menu_.options->setItemCaption(i, Lang::get()->getText(60) + ": " + Lang::get()->getText(72)); // LINEAL
}
i++;
// VSYNC
if (Options::video.vsync) {
menu_.options->setItemCaption(i, Lang::get()->getText(61) + ": " + Lang::get()->getText(73)); // ON
} else {
menu_.options->setItemCaption(i, Lang::get()->getText(61) + ": " + Lang::get()->getText(74)); // OFF
}
i++;
// HOW TO PLAY
menu_.options->setItemCaption(i, Lang::get()->getText(2));
i++;
// ACCEPT
menu_.options->setItemCaption(i, Lang::get()->getText(9)); // ACCEPT
i++;
// CANCEL
menu_.options->setItemCaption(i, Lang::get()->getText(10)); // CANCEL
// Recoloca el menu de opciones
menu_.options->centerMenuOnX(GAMECANVAS_CENTER_X);
menu_.options->centerMenuOnY(GAMECANVAS_CENTER_Y);
menu_.options->centerMenuElementsOnX();
// Establece las etiquetas del menu de titulo
#ifdef GAME_CONSOLE
menu_.title->setItemCaption(0, Lang::get()->getText(0)); // PLAY
#else
menu_.title->setItemCaption(0, Lang::get()->getText(51)); // 1 PLAYER
#endif
menu_.title->setItemCaption(1, Lang::get()->getText(52)); // 2 PLAYERS
menu_.title->setItemCaption(2, Lang::get()->getText(1)); // OPTIONS
menu_.title->setItemCaption(3, Lang::get()->getText(3)); // QUIT
#ifdef __EMSCRIPTEN__
menu_.title->setVisible(3, false);
menu_.title->setSelectable(3, false);
#endif
// Recoloca el menu de titulo
menu_.title->centerMenuOnX(GAMECANVAS_CENTER_X);
menu_.title->centerMenuElementsOnX();
// Establece las etiquetas del menu de seleccion de jugador
menu_.player_select->setItemCaption(0, Lang::get()->getText(39)); // SELECT PLAYER
menu_.player_select->setItemCaption(3, Lang::get()->getText(40)); // BACK
// Recoloca el menu de selección de jugador
menu_.player_select->centerMenuOnX(GAMECANVAS_CENTER_X);
menu_.player_select->centerMenuElementsOnX();
#ifdef GAME_CONSOLE
menu_.options->setGreyed(1, true);
menu_.options->setSelectable(1, false);
menu_.options->setGreyed(2, true);
menu_.options->setSelectable(2, false);
menu_.options->setGreyed(3, true);
menu_.options->setSelectable(3, false);
menu_.options->setGreyed(4, true);
menu_.options->setSelectable(4, false);
#endif
}
// Aplica las opciones de menu seleccionadas
void Title::applyOptions() {
Screen::get()->setVideoMode(Options::video.fullscreen);
Screen::get()->setWindowZoom(Options::window.zoom);
Screen::get()->setVSync(Options::video.vsync);
Lang::get()->setLang(Options::settings.language);
updateMenuLabels();
createTiledBackground();
}
// 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
if (instructions_active_) {
instructions_->update(DELTA_TIME_S);
instructions_->render();
if (instructions_->hasFinished()) {
bool was_quit = instructions_->isQuitRequested();
delete instructions_;
instructions_ = nullptr;
instructions_active_ = false;
if (was_quit) {
section_->name = SECTION_PROG_QUIT;
} else if (instructions_mode_ == Instructions::Mode::AUTO) {
section_->name = SECTION_PROG_TITLE;
init();
demo_ = true;
} else {
section_->name = SECTION_PROG_TITLE;
section_->subsection = SUBSECTION_TITLE_3;
}
// Reset del rellotge per evitar un dt enorme al tornar al Title.
DeltaTime::reset();
}
return;
}
// Ejecución normal del título
update(DELTA_TIME_S);
render();
}
// Procesa un evento individual
void Title::handleEvent(const SDL_Event *event) {
// Si hay un sub-estado activo, delega el evento
if (instructions_active_ && (instructions_ != nullptr)) {
// SDL_EVENT_QUIT ya lo maneja Director
return;
}
if (demo_game_active_ && (demo_game_ != nullptr)) {
demo_game_->handleEvent(event);
return;
}
// SDL_EVENT_QUIT ya lo maneja Director
if (event->type == SDL_EVENT_RENDER_DEVICE_RESET || event->type == SDL_EVENT_RENDER_TARGETS_RESET) {
reLoadTextures();
}
if (section_->subsection == SUBSECTION_TITLE_3) {
if ((event->type == SDL_EVENT_KEY_UP) || (event->type == SDL_EVENT_JOYSTICK_BUTTON_UP)) {
menu_visible_ = true;
demo_remaining_s_ = DEMO_TIMEOUT_S;
}
}
}
// Bucle para el titulo del juego (compatibilidad)
void Title::run() {
while (section_->name == SECTION_PROG_TITLE || instructions_active_ || demo_game_active_) {
iterate();
}
}
// Inicia la parte donde se muestran las instrucciones
void Title::runInstructions(Instructions::Mode mode) {
instructions_ = new Instructions(renderer_, section_);
instructions_->start(mode);
instructions_active_ = true;
instructions_mode_ = mode;
}
// Inicia el juego en modo demo
void Title::runDemoGame() {
// Temporalmente ponemos section para que el constructor de Game funcione
section_->name = SECTION_PROG_GAME;
section_->subsection = SUBSECTION_GAME_PLAY_1P;
demo_game_ = new Game(1, 0, renderer_, true, section_);
demo_game_active_ = true;
// Restauramos section para que Director no transicione fuera de Title
section_->name = SECTION_PROG_TITLE;
}
// Modifica las opciones para los controles de los jugadores
auto Title::updatePlayerInputs(int num_player) -> bool {
const int NUM_DEVICES = available_input_devices_.size();
if (!Input::get()->gameControllerFound()) { // Si no hay mandos se deja todo de manera prefijada
device_index_[0] = 0;
device_index_[1] = 0;
Options::inputs[0].id = -1;
Options::inputs[0].name = "KEYBOARD";
Options::inputs[0].device_type = Input::Device::KEYBOARD;
Options::inputs[1].id = 0;
Options::inputs[1].name = "GAME CONTROLLER";
Options::inputs[1].device_type = Input::Device::GAMECONTROLLER;
return true;
} // Si hay mas de un dispositivo, se recorre el vector
if (Options::settings.console) {
std::cout << "numplayer:" << num_player << '\n';
std::cout << "deviceindex:" << device_index_[num_player] << '\n';
}
// Incrementa el indice
if (device_index_[num_player] < NUM_DEVICES - 1) {
device_index_[num_player]++;
} else {
device_index_[num_player] = 0;
}
if (Options::settings.console) {
std::cout << "deviceindex:" << device_index_[num_player] << '\n';
}
// Si coincide con el del otro jugador, se lo intercambian
if (device_index_[0] == device_index_[1]) {
const int THE_OTHER_PLAYER = (num_player + 1) % 2;
device_index_[THE_OTHER_PLAYER]--;
if (device_index_[THE_OTHER_PLAYER] < 0) {
device_index_[THE_OTHER_PLAYER] = NUM_DEVICES - 1;
}
}
// Copia el dispositivo marcado por el indice a la variable de opciones de cada jugador
Options::inputs[0] = available_input_devices_[device_index_[0]];
Options::inputs[1] = available_input_devices_[device_index_[1]];
return true;
}
// Crea el mosaico de fondo del titulo
void Title::createTiledBackground() {
// Crea la textura para el mosaico de fondo
background_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH * 2, GAMECANVAS_HEIGHT * 2);
if (background_ != nullptr) {
SDL_SetTextureScaleMode(background_, Texture::current_scale_mode);
}
if (background_ == nullptr) {
if (Options::settings.console) {
std::cout << "TitleSurface could not be created!\nSDL Error: " << SDL_GetError() << '\n';
}
}
// Crea los objetos para pintar en la textura de fondo
auto *bg_tile_texture = new Texture(renderer_, Asset::get()->get("title_bg_tile.png"));
auto *tile = new Sprite({0, 0, 64, 64}, bg_tile_texture, renderer_);
// Prepara para dibujar sobre la textura
SDL_SetRenderTarget(renderer_, background_);
SDL_SetRenderDrawColor(renderer_, 0x43, 0x43, 0x4F, 0xFF);
SDL_RenderClear(renderer_);
// Rellena la textura con el tile
tile->setSpriteClip(0, 0, 64, 64);
for (int i = 0; i < 8; ++i) {
for (int j = 0; j < 6; ++j) {
tile->setPosX(i * 64);
tile->setPosY(j * 64);
tile->render();
}
}
// Vuelve a colocar el renderizador apuntando a la pantalla
SDL_SetRenderTarget(renderer_, nullptr);
// Libera la memoria utilizada por los objetos
bg_tile_texture->unload();
delete bg_tile_texture;
delete tile;
}
// Comprueba cuantos mandos hay conectados para gestionar el menu de opciones.
// El estado de Input lo mantiene al día Director via eventos SDL_EVENT_GAMEPAD_ADDED/REMOVED,
// así que aquí solo leemos la lista actual sin reescanear.
void Title::checkInputDevices() {
if (Options::settings.console) {
std::cout << "Filling devices for options menu_..." << '\n';
}
const int NUM_CONTROLLERS = Input::get()->getNumControllers();
available_input_devices_.clear();
InputDevice temp;
// Añade todos los mandos
if (NUM_CONTROLLERS > 0) {
for (int i = 0; i < NUM_CONTROLLERS; ++i) {
temp.id = i;
temp.name = Input::get()->getControllerName(i);
temp.device_type = Input::Device::GAMECONTROLLER;
available_input_devices_.push_back(temp);
if (Options::settings.console) {
std::cout << "Device " << (int)available_input_devices_.size() << " - " << temp.name.c_str() << '\n';
}
}
}
// Añade el teclado al final
temp.id = -1;
temp.name = "KEYBOARD";
temp.device_type = Input::Device::KEYBOARD;
available_input_devices_.push_back(temp);
if (Options::settings.console) {
std::cout << "Device " << (int)available_input_devices_.size() << " - " << temp.name.c_str() << '\n';
std::cout << '\n';
}
}
// Recarga las texturas
void Title::reLoadTextures() {
dust_texture_->reLoad();
coffee_texture_->reLoad();
crisis_texture_->reLoad();
gradient_texture_->reLoad();
createTiledBackground();
}