#include "game/game.h" #include #include // for max, min #include // for rand #include // for basic_ostream, char_traits, operator<< #include // for accumulate #include "core/audio/audio.hpp" // for Audio #include "core/input/global_inputs.hpp" // for GlobalInputs::handle #include "core/input/input.h" // for InputAction, Input, Input::Repeat::ON, Input::Repeat::OFF #include "core/locale/lang.h" // for Lang #include "core/rendering/fade.h" // for Fade #include "core/rendering/movingsprite.h" // for MovingSprite #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 #include "game/defaults.hpp" // for PLAY_AREA_CENTER_X, BLOCK, PLAY_AREA_CEN... #include "game/entities/balloon.h" // for Balloon, Balloon::VELX_NEGATIVE, BALLOON_... #include "game/entities/bullet.h" // for Bullet, Bullet::Kind::LEFT, Bullet::Kind::RIGHT, BULLE... #include "game/entities/item.h" // for Item #include "game/entities/player.h" // for Player #include "game/options.hpp" // for Options #include "game/ui/menu.h" // for Menu namespace Ja { struct Sound; } // namespace Ja namespace { // Constantes geométricas y temporales compartidas por los helpers de initEnemyFormations constexpr int Y4 = (PLAY_AREA_TOP - BLOCK); constexpr int X4_0 = PLAY_AREA_LEFT; constexpr int X4_100 = PLAY_AREA_RIGHT - Balloon::WIDTH_4; constexpr int Y3 = (PLAY_AREA_TOP - BLOCK); constexpr int X3_0 = PLAY_AREA_LEFT; constexpr int X3_100 = PLAY_AREA_RIGHT - Balloon::WIDTH_3; constexpr int Y2 = (PLAY_AREA_TOP - BLOCK); constexpr int X2_0 = PLAY_AREA_LEFT; constexpr int X2_100 = PLAY_AREA_RIGHT - Balloon::WIDTH_2; constexpr int Y1 = (PLAY_AREA_TOP - BLOCK); constexpr int X1_0 = PLAY_AREA_LEFT; constexpr int X1_50 = PLAY_AREA_CENTER_X - (Balloon::WIDTH_1 / 2); constexpr int X1_100 = PLAY_AREA_RIGHT - Balloon::WIDTH_1; constexpr Uint16 CREATION_TIME = 300; } // namespace // Constructor Game::Game(int num_players, int current_stage, SDL_Renderer *renderer, bool demo, Section *section) : last_stage_reached_(current_stage) { // Copia los punteros this->renderer_ = renderer; this->section_ = section; // Pasa variables this->demo_.enabled = demo; this->num_players_ = num_players; this->current_stage_ = current_stage; if (num_players == 1) { // Si solo juega un jugador, permite jugar tanto con teclado como con mando player_one_control_ = Options::inputs[0].device_type; Options::inputs[0].device_type = Input::Device::ANY; } difficulty_ = Options::settings.difficulty; // Crea los objetos fade_ = new Fade(renderer); event_handler_ = new SDL_Event(); // Carga los recursos loadMedia(); // Carga ficheros loadDemoFile(); // Establece la máxima puntuación desde fichero o desde las puntuaciones online setHiScore(); clouds1_a_ = new MovingSprite(0, 0, 256, 52, -0.4F, 0.0F, 0.0F, 0.0F, game_clouds_texture_, renderer); clouds1_b_ = new MovingSprite(256, 0, 256, 52, -0.4F, 0.0F, 0.0F, 0.0F, game_clouds_texture_, renderer); clouds2_a_ = new MovingSprite(0, 52, 256, 32, -0.2F, 0.0F, 0.0F, 0.0F, game_clouds_texture_, renderer); clouds2_b_ = new MovingSprite(256, 52, 256, 32, -0.2F, 0.0F, 0.0F, 0.0F, game_clouds_texture_, renderer); n1000_sprite_ = new SmartSprite(game_text_texture_, renderer); n2500_sprite_ = new SmartSprite(game_text_texture_, renderer); n5000_sprite_ = new SmartSprite(game_text_texture_, renderer); buildings_sprite_ = new Sprite(0, 0, 256, 160, game_buildings_texture_, renderer); sky_colors_sprite_ = new Sprite(0, 0, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT, game_sky_colors_texture_, renderer); grass_sprite_ = new Sprite(0, 0, 256, 6, game_grass_texture_, renderer); power_meter_sprite_ = new Sprite(PLAY_AREA_CENTER_X - 20, 170, 40, 7, game_power_meter_texture_, renderer); game_over_sprite_ = new Sprite(16, 80, 128, 96, game_over_texture_, renderer); game_over_end_sprite_ = new Sprite(PLAY_AREA_CENTER_X - (game_over_end_texture_->getWidth() / 2), 80, 128, 96, game_over_end_texture_, renderer); // Inicializa las variables necesarias para la sección 'Game' init(); // Reset del rellotge perquè el primer dt_s no inclogui el temps de càrrega. DeltaTime::reset(); } Game::~Game() { saveScoreFile(); saveDemoFile(); // Restaura el metodo de control if (num_players_ == 1) { Options::inputs[0].device_type = player_one_control_; } // Elimina todos los objetos contenidos en vectores (jugadores, balas, etc.) deleteAllVectorObjects(); // Las texturas, animaciones, Text, Menu, sonidos y música son propiedad // de Resource — no se liberan aquí. Solo limpiamos nuestras vistas. player_animations_.clear(); balloon_animations_.clear(); item_animations_.clear(); player1_textures_.clear(); player2_textures_.clear(); item_textures_.clear(); balloon_textures_.clear(); delete fade_; delete event_handler_; delete clouds1_a_; delete clouds1_b_; delete clouds2_a_; delete clouds2_b_; delete n1000_sprite_; delete n2500_sprite_; delete n5000_sprite_; delete buildings_sprite_; delete sky_colors_sprite_; delete grass_sprite_; delete power_meter_sprite_; delete game_over_sprite_; delete game_over_end_sprite_; } // Inicializa las variables necesarias para la sección 'Game' void Game::init() { ticks_ = 0; ticks_speed_ = 15; // Elimina qualquier jugador que hubiese antes de crear los nuevos for (auto *player : players_) { delete player; }; players_.clear(); // Crea los jugadores if (num_players_ == 1) { auto *player = new Player(PLAY_AREA_CENTER_X - 11, PLAY_AREA_BOTTOM - 24, renderer_, player_textures_[Options::settings.player_selected], player_animations_); players_.push_back(player); } else if (num_players_ == 2) { auto *player1 = new Player((PLAY_AREA_CENTER_FIRST_QUARTER_X * ((0 * 2) + 1)) - 11, PLAY_AREA_BOTTOM - 24, renderer_, player_textures_[0], player_animations_); auto *player2 = new Player((PLAY_AREA_CENTER_FIRST_QUARTER_X * ((1 * 2) + 1)) - 11, PLAY_AREA_BOTTOM - 24, renderer_, player_textures_[1], player_animations_); players_.push_back(player1); players_.push_back(player2); } // Inicializa las variables switch (difficulty_) { case DIFFICULTY_EASY: default_enemy_speed_ = Balloon::SPEED_1; difficulty_score_multiplier_ = 0.5F; difficulty_color_ = {75, 105, 47}; pause_menu_->setSelectorColor(difficulty_color_, 255); game_over_menu_->setSelectorColor(difficulty_color_, 255); break; case DIFFICULTY_NORMAL: default_enemy_speed_ = Balloon::SPEED_1; difficulty_score_multiplier_ = 1.0F; difficulty_color_ = {255, 122, 0}; pause_menu_->setSelectorColor(difficulty_color_, 255); game_over_menu_->setSelectorColor(difficulty_color_, 255); break; case DIFFICULTY_HARD: default_enemy_speed_ = Balloon::SPEED_5; difficulty_score_multiplier_ = 1.5F; difficulty_color_ = {118, 66, 138}; pause_menu_->setSelectorColor(difficulty_color_, 255); game_over_menu_->setSelectorColor(difficulty_color_, 255); break; default: break; } game_completed_ = false; game_completed_counter_ = 0; game_completed_counter_s_ = 0.0F; section_->name = SECTION_PROG_GAME; section_->subsection = SUBSECTION_GAME_PLAY_1P; menace_current_ = 0; menace_threshold_ = 0; hi_score_achieved_ = false; stage_bitmap_counter_ = STAGE_COUNTER; stage_bitmap_counter_s_ = STAGE_COUNTER / 60.0F; death_counter_ = Player::DEATH_COUNTER; death_counter_s_ = Player::DEATH_COUNTER / 60.0F; time_stopped_ = false; time_stopped_counter_ = 0; time_stopped_counter_s_ = 0.0F; counter_ = 0; elapsed_s_ = 0.0F; last_enemy_deploy_ = 0; enemy_deploy_counter_ = 0; enemy_deploy_counter_s_ = 0.0F; enemy_deploy_phase_s_ = 0.0F; shake_phase_s_ = 0.0F; helper_counter_s_ = 0.0F; enemy_speed_ = default_enemy_speed_; effect_.flash = false; effect_.shake = false; effect_.shake_counter = SHAKE_COUNTER; death_shake_.active = false; death_shake_.step = 0; death_shake_.last_step_ticks = 0; death_sequence_.phase = DeathPhase::NONE; death_sequence_.phase_start_ticks = 0; death_sequence_.player = nullptr; helper_.need_coffee = false; helper_.need_coffee_machine = false; helper_.need_power_ball = false; helper_.counter = HELP_COUNTER; helper_.item_disk_odds = ITEM_POINTS_1_DISK_ODDS; helper_.item_gavina_odds = ITEM_POINTS_2_GAVINA_ODDS; helper_.item_paco_odds = ITEM_POINTS_3_PACMAR_ODDS; helper_.item_clock_odds = ITEM_CLOCK_ODDS; helper_.item_coffee_odds = ITEM_COFFEE_ODDS; helper_.item_coffee_machine_odds = ITEM_COFFEE_MACHINE_ODDS; power_ball_enabled_ = false; power_ball_counter_ = 0; coffee_machine_enabled_ = false; pause_counter_ = 0; leaving_pause_menu_ = false; pause_initialized_ = false; game_over_initialized_ = false; game_over_post_fade_ = 0; if (demo_.enabled) { const int NUM = rand() % 2; if (NUM == 0) { balloons_popped_ = 1000; current_stage_ = 3; } else { balloons_popped_ = 1800; current_stage_ = 6; } } initPaths(); initEnemyFormations(); initEnemyPools(); initGameStages(); // Mas variables balloons_popped_ = 0; for (int i = 0; i < current_stage_; ++i) { balloons_popped_ += stage_[i].power_to_complete; } total_power_to_complete_game_ = std::accumulate(std::begin(stage_), std::end(stage_), 0, [](int acc, const auto &s) { return acc + s.power_to_complete; }); // Modo demo demo_.recording = false; demo_.counter = 0; // Inicializa el objeto para el fundido fade_->init(0x27, 0x27, 0x36); // Sprites clouds1_a_->setSpriteClip(0, 0, 256, 52); clouds1_b_->setSpriteClip(0, 0, 256, 52); clouds2_a_->setSpriteClip(0, 52, 256, 32); clouds2_b_->setSpriteClip(0, 52, 256, 32); grass_sprite_->setPosY(154); power_meter_sprite_->setSpriteClip(0, 0, 40, 7); // Con los globos creados, calcula el nivel de amenaza evaluateAndSetMenace(); // Inicializa el bitmap de 1000 puntos (px/s i px/s²; -0.5 px/frame → -30 px/s) n1000_sprite_->setPosX(0); n1000_sprite_->setPosY(0); n1000_sprite_->setWidth(26); n1000_sprite_->setHeight(9); n1000_sprite_->setVelX(0.0F); n1000_sprite_->setVelY(-30.0F); n1000_sprite_->setAccelX(0.0F); n1000_sprite_->setAccelY(-360.0F); n1000_sprite_->setSpriteClip(0, 0, 26, 9); n1000_sprite_->setEnabled(false); n1000_sprite_->setEnabledCounter(0); n1000_sprite_->setDestX(0); n1000_sprite_->setDestY(0); // Inicializa el bitmap de 2500 puntos n2500_sprite_->setPosX(0); n2500_sprite_->setPosY(0); n2500_sprite_->setWidth(28); n2500_sprite_->setHeight(9); n2500_sprite_->setVelX(0.0F); n2500_sprite_->setVelY(-30.0F); n2500_sprite_->setAccelX(0.0F); n2500_sprite_->setAccelY(-360.0F); n2500_sprite_->setSpriteClip(26, 0, 28, 9); n2500_sprite_->setEnabled(false); n2500_sprite_->setEnabledCounter(0); n2500_sprite_->setDestX(0); n2500_sprite_->setDestY(0); // Inicializa el bitmap de 5000 puntos n5000_sprite_->setPosX(0); n5000_sprite_->setPosY(0); n5000_sprite_->setWidth(28); n5000_sprite_->setHeight(9); n5000_sprite_->setVelX(0.0F); n5000_sprite_->setVelY(-30.0F); n5000_sprite_->setAccelX(0.0F); n5000_sprite_->setAccelY(-360.0F); n5000_sprite_->setSpriteClip(54, 0, 28, 9); n5000_sprite_->setEnabled(false); n5000_sprite_->setEnabledCounter(0); n5000_sprite_->setDestX(0); n5000_sprite_->setDestY(0); // Los fondos sky_colors_rect_[0] = {.x = 0, .y = 0, .w = GAMECANVAS_WIDTH, .h = GAMECANVAS_HEIGHT}; sky_colors_rect_[1] = {.x = 256, .y = 0, .w = GAMECANVAS_WIDTH, .h = GAMECANVAS_HEIGHT}; sky_colors_rect_[2] = {.x = 0, .y = 192, .w = GAMECANVAS_WIDTH, .h = GAMECANVAS_HEIGHT}; sky_colors_rect_[3] = {.x = 256, .y = 192, .w = GAMECANVAS_WIDTH, .h = GAMECANVAS_HEIGHT}; } // Carga los recursos necesarios para la sección 'Game' void Game::loadMedia() { if (Options::settings.console) { std::cout << '\n' << "** LOADING RESOURCES FOR GAME SECTION" << '\n'; } Resource *resource = Resource::get(); // Texturas (handles compartidos — no se liberan aquí) bullet_texture_ = resource->getTexture("bullet.png"); game_buildings_texture_ = resource->getTexture("game_buildings.png"); game_clouds_texture_ = resource->getTexture("game_clouds.png"); game_grass_texture_ = resource->getTexture("game_grass.png"); game_power_meter_texture_ = resource->getTexture("game_power_meter.png"); game_sky_colors_texture_ = resource->getTexture("game_sky_colors.png"); game_text_texture_ = resource->getTexture("game_text.png"); game_over_texture_ = resource->getTexture("menu_game_over.png"); game_over_end_texture_ = resource->getTexture("menu_game_over_end.png"); // Texturas - Globos balloon_textures_.push_back(resource->getTexture("balloon1.png")); balloon_textures_.push_back(resource->getTexture("balloon2.png")); balloon_textures_.push_back(resource->getTexture("balloon3.png")); balloon_textures_.push_back(resource->getTexture("balloon4.png")); // Texturas - Items item_textures_.push_back(resource->getTexture("item_points1_disk.png")); item_textures_.push_back(resource->getTexture("item_points2_gavina.png")); item_textures_.push_back(resource->getTexture("item_points3_pacmar.png")); item_textures_.push_back(resource->getTexture("item_clock.png")); item_textures_.push_back(resource->getTexture("item_coffee.png")); item_textures_.push_back(resource->getTexture("item_coffee_machine.png")); // Texturas - Player1 player1_textures_.push_back(resource->getTexture("player_bal1_head.png")); player1_textures_.push_back(resource->getTexture("player_bal1_body.png")); player1_textures_.push_back(resource->getTexture("player_bal1_legs.png")); player1_textures_.push_back(resource->getTexture("player_bal1_death.png")); player1_textures_.push_back(resource->getTexture("player_bal1_fire.png")); player_textures_.push_back(player1_textures_); // Texturas - Player2 player2_textures_.push_back(resource->getTexture("player_arounder_head.png")); player2_textures_.push_back(resource->getTexture("player_arounder_body.png")); player2_textures_.push_back(resource->getTexture("player_arounder_legs.png")); player2_textures_.push_back(resource->getTexture("player_arounder_death.png")); player2_textures_.push_back(resource->getTexture("player_arounder_fire.png")); player_textures_.push_back(player2_textures_); // Animaciones (handles a los vectores raw de Resource — no se liberan) player_animations_.push_back(&resource->getAnimationLines("player_head.ani")); player_animations_.push_back(&resource->getAnimationLines("player_body.ani")); player_animations_.push_back(&resource->getAnimationLines("player_legs.ani")); player_animations_.push_back(&resource->getAnimationLines("player_death.ani")); player_animations_.push_back(&resource->getAnimationLines("player_fire.ani")); balloon_animations_.push_back(&resource->getAnimationLines("balloon1.ani")); balloon_animations_.push_back(&resource->getAnimationLines("balloon2.ani")); balloon_animations_.push_back(&resource->getAnimationLines("balloon3.ani")); balloon_animations_.push_back(&resource->getAnimationLines("balloon4.ani")); item_animations_.push_back(&resource->getAnimationLines("item_points1_disk.ani")); item_animations_.push_back(&resource->getAnimationLines("item_points2_gavina.ani")); item_animations_.push_back(&resource->getAnimationLines("item_points3_pacmar.ani")); item_animations_.push_back(&resource->getAnimationLines("item_clock.ani")); item_animations_.push_back(&resource->getAnimationLines("item_coffee.ani")); item_animations_.push_back(&resource->getAnimationLines("item_coffee_machine.ani")); // Texto text_ = resource->getText("smb2"); text_scoreboard_ = resource->getText("8bithud"); text_big_ = resource->getText("smb2_big"); text_nokia2_ = resource->getText("nokia2"); text_nokia_big2_ = resource->getText("nokia_big2"); // Menus game_over_menu_ = resource->getMenu("gameover"); game_over_menu_->setItemCaption(0, Lang::get()->getText(48)); game_over_menu_->setItemCaption(1, Lang::get()->getText(49)); const int W = text_->getCharacterSize() * Lang::get()->getText(45).length(); game_over_menu_->setRectSize(W, 0); game_over_menu_->centerMenuOnX(199); pause_menu_ = resource->getMenu("pause"); pause_menu_->setItemCaption(0, Lang::get()->getText(41)); pause_menu_->setItemCaption(1, Lang::get()->getText(46)); pause_menu_->setItemCaption(2, Lang::get()->getText(47)); // Sonidos balloon_sound_ = resource->getSound("balloon.wav"); bubble1_sound_ = resource->getSound("bubble1.wav"); bubble2_sound_ = resource->getSound("bubble2.wav"); bubble3_sound_ = resource->getSound("bubble3.wav"); bubble4_sound_ = resource->getSound("bubble4.wav"); bullet_sound_ = resource->getSound("bullet.wav"); clock_sound_ = resource->getSound("clock.wav"); coffee_out_sound_ = resource->getSound("coffeeout.wav"); hi_score_sound_ = resource->getSound("hiscore.wav"); item_drop_sound_ = resource->getSound("itemdrop.wav"); item_pick_up_sound_ = resource->getSound("itempickup.wav"); player_collision_sound_ = resource->getSound("player_collision.wav"); power_ball_sound_ = resource->getSound("powerball.wav"); stage_change_sound_ = resource->getSound("stage_change.wav"); coffee_machine_sound_ = resource->getSound("title.wav"); // Musicas game_music_ = resource->getMusic("playing.ogg"); if (Options::settings.console) { std::cout << "** RESOURCES FOR GAME SECTION LOADED" << '\n' << '\n'; } } // Carga el fichero de puntos auto Game::loadScoreFile() -> bool { // Indicador de éxito en la carga bool success = true; const std::string P = Asset::get()->get("score.bin"); const std::string FILE_NAME = P.substr(P.find_last_of("\\/") + 1); SDL_IOStream *file = SDL_IOFromFile(P.c_str(), "r+b"); // El fichero no existe if (file == nullptr) { if (Options::settings.console) { std::cout << "Warning: Unable to open " << FILE_NAME.c_str() << " file" << '\n'; } // Creamos el fichero para escritura file = SDL_IOFromFile(P.c_str(), "w+b"); if (file != nullptr) { if (Options::settings.console) { std::cout << "New file (" << FILE_NAME.c_str() << ") created!" << '\n'; } // Inicializamos los datos for (unsigned int &i : score_data_file_) { i = 0; SDL_WriteIO(file, &i, sizeof(Uint32)); } // Cerramos el fichero SDL_CloseIO(file); } else { if (Options::settings.console) { std::cout << "Error: Unable to create file " << FILE_NAME.c_str() << '\n'; } success = false; } } // El fichero existe else { // Cargamos los datos if (Options::settings.console) { std::cout << "Reading file " << FILE_NAME.c_str() << '\n'; } for (unsigned int &i : score_data_file_) { SDL_ReadIO(file, &i, sizeof(Uint32)); } // Cierra el fichero SDL_CloseIO(file); } // Estableix el hi-score si el fitxer és vàlid (≠ 0) i el checksum quadra; // en qualsevol altre cas (fitxer absent, corrupte o manipulat) usa el default. if (score_data_file_[0] != 0 && score_data_file_[0] % 43 == score_data_file_[1]) { hi_score_ = score_data_file_[0]; } else { hi_score_ = 10000; } return success; } // Carga el fichero de datos para la demo 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; } } return true; } // Guarda el fichero de puntos auto Game::saveScoreFile() -> bool { bool success = true; const std::string P = Asset::get()->get("score.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) { // Guardamos los datos for (unsigned int &i : score_data_file_) { SDL_WriteIO(file, &i, sizeof(Uint32)); } 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'; } } return success; } // Guarda el fichero de datos para la demo 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'; } } } return success; } // Inicializa las formaciones enemigas void Game::initEnemyFormations() { initEnemyFormationsZero(); initEnemyFormationsLinear(); initEnemyFormationsSymmetric(); initEnemyFormationsHexagonsAndTest(); } // Pone a cero todas las formaciones void Game::initEnemyFormationsZero() { for (auto &i : enemy_formation_) { i.number_of_enemies = 0; for (auto &j : i.init) { j.x = 0; j.y = 0; j.vel_x = 0; j.kind = 0; j.creation_counter = 0; } } } // Formaciones 0..19: bucles lineales (todos los enemigos van en la misma dirección) void Game::initEnemyFormationsLinear() { int inc_x = 0; Uint8 inc_time = 0; Uint8 j = 0; // #00 - Dos enemigos BALLOON4 uno a cada extremo j = 0; enemy_formation_[j].number_of_enemies = 2; inc_x = X4_100; inc_time = 0; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X4_0 + (i * inc_x); enemy_formation_[j].init[i].y = Y4; enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE * (((i % 2) * 2) - 1); enemy_formation_[j].init[i].kind = Balloon::BALLOON_4; enemy_formation_[j].init[i].creation_counter = CREATION_TIME + (inc_time * i); } // #01 - Dos enemigos BALLOON4 uno a cada cuarto. Ambos van hacia el centro j = 1; enemy_formation_[j].number_of_enemies = 2; inc_x = PLAY_AREA_CENTER_X; inc_time = 0; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = PLAY_AREA_CENTER_FIRST_QUARTER_X - (Balloon::WIDTH_4 / 2) + (i * inc_x); enemy_formation_[j].init[i].y = Y4; enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE * (((i % 2) * 2) - 1); enemy_formation_[j].init[i].kind = Balloon::BALLOON_4; enemy_formation_[j].init[i].creation_counter = CREATION_TIME + (inc_time * i); } // #02 - Cuatro enemigos BALLOON2 uno detras del otro. A la izquierda y hacia el centro j = 2; enemy_formation_[j].number_of_enemies = 4; inc_x = Balloon::WIDTH_2 + 1; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X2_0 + (i * inc_x); enemy_formation_[j].init[i].y = Y2; enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_2; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #03 - Cuatro enemigos BALLOON2 uno detras del otro. A la derecha y hacia el centro j = 3; enemy_formation_[j].number_of_enemies = 4; inc_x = Balloon::WIDTH_2 + 1; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X2_100 - (i * inc_x); enemy_formation_[j].init[i].y = Y2; enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_2; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #04 - Tres enemigos BALLOON3. 0, 25, 50. Hacia la derecha j = 4; enemy_formation_[j].number_of_enemies = 3; inc_x = Balloon::WIDTH_3 * 2; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X3_0 + (i * inc_x); enemy_formation_[j].init[i].y = Y3; enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_3; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #05 - Tres enemigos BALLOON3. 50, 75, 100. Hacia la izquierda j = 5; enemy_formation_[j].number_of_enemies = 3; inc_x = Balloon::WIDTH_3 * 2; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X3_100 - (i * inc_x); enemy_formation_[j].init[i].y = Y3; enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_3; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #06 - Tres enemigos BALLOON3. 0, 0, 0. Hacia la derecha j = 6; enemy_formation_[j].number_of_enemies = 3; inc_x = Balloon::WIDTH_3 + 1; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X3_0 + (i * inc_x); enemy_formation_[j].init[i].y = Y3; enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_3; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #07 - Tres enemigos BALLOON3. 100, 100, 100. Hacia la izquierda j = 7; enemy_formation_[j].number_of_enemies = 3; inc_x = Balloon::WIDTH_3 + 1; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X3_100 - (i * inc_x); enemy_formation_[j].init[i].y = Y3; enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_3; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #08 - Seis enemigos BALLOON1. 0, 0, 0, 0, 0, 0. Hacia la derecha j = 8; enemy_formation_[j].number_of_enemies = 6; inc_x = Balloon::WIDTH_1 + 1; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X1_0 + (i * inc_x); enemy_formation_[j].init[i].y = 13; enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_1; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #09 - Seis enemigos BALLOON1. 100, 100, 100, 100, 100, 100. Hacia la izquierda j = 9; enemy_formation_[j].number_of_enemies = 6; inc_x = Balloon::WIDTH_1 + 1; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X1_100 - (i * inc_x); enemy_formation_[j].init[i].y = 13; enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_1; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #10 - Tres enemigos BALLOON4 seguidos desde la izquierda j = 10; enemy_formation_[j].number_of_enemies = 3; inc_x = Balloon::WIDTH_4 + 1; inc_time = 15; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X4_0 + (i * inc_x); enemy_formation_[j].init[i].y = Y4; enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_4; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #11 - Tres enemigos BALLOON4 seguidos desde la derecha j = 11; enemy_formation_[j].number_of_enemies = 3; inc_x = Balloon::WIDTH_4 + 1; inc_time = 15; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X4_100 - (i * inc_x); enemy_formation_[j].init[i].y = Y4; enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_4; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #12 - Seis enemigos BALLOON2 uno detras del otro. A la izquierda y hacia el centro j = 12; enemy_formation_[j].number_of_enemies = 6; inc_x = Balloon::WIDTH_2 + 1; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X2_0 + (i * inc_x); enemy_formation_[j].init[i].y = Y2; enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_2; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #13 - Seis enemigos BALLOON2 uno detras del otro. A la derecha y hacia el centro j = 13; enemy_formation_[j].number_of_enemies = 6; inc_x = Balloon::WIDTH_2 + 1; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X2_100 - (i * inc_x); enemy_formation_[j].init[i].y = Y2; enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_2; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #14 - Cinco enemigos BALLOON3. Hacia la derecha. Separados j = 14; enemy_formation_[j].number_of_enemies = 5; inc_x = Balloon::WIDTH_3 * 2; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X3_0 + (i * inc_x); enemy_formation_[j].init[i].y = Y3; enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_3; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #15 - Cinco enemigos BALLOON3. Hacia la izquierda. Separados j = 15; enemy_formation_[j].number_of_enemies = 5; inc_x = Balloon::WIDTH_3 * 2; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X3_100 - (i * inc_x); enemy_formation_[j].init[i].y = Y3; enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_3; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #16 - Cinco enemigos BALLOON3. Hacia la derecha. Juntos j = 16; enemy_formation_[j].number_of_enemies = 5; inc_x = Balloon::WIDTH_3 + 1; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X3_0 + (i * inc_x); enemy_formation_[j].init[i].y = Y3; enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_3; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #17 - Cinco enemigos BALLOON3. Hacia la izquierda. Juntos j = 17; enemy_formation_[j].number_of_enemies = 5; inc_x = Balloon::WIDTH_3 + 1; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X3_100 - (i * inc_x); enemy_formation_[j].init[i].y = Y3; enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_3; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #18 - Doce enemigos BALLOON1. Hacia la derecha. Juntos j = 18; enemy_formation_[j].number_of_enemies = 12; inc_x = Balloon::WIDTH_1 + 1; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X1_0 + (i * inc_x); enemy_formation_[j].init[i].y = Y1; enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_1; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } // #19 - Doce enemigos BALLOON1. Hacia la izquierda. Juntos j = 19; enemy_formation_[j].number_of_enemies = 12; inc_x = Balloon::WIDTH_1 + 1; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { enemy_formation_[j].init[i].x = X1_100 - (i * inc_x); enemy_formation_[j].init[i].y = Y1; enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].kind = Balloon::BALLOON_1; enemy_formation_[j].init[i].creation_counter = CREATION_TIME - (inc_time * i); } } // Formaciones 20..25: simétricas (la primera mitad va hacia un lado, la segunda hacia el otro) void Game::initEnemyFormationsSymmetric() { int inc_x = 0; Uint8 inc_time = 0; Uint8 j = 0; // #20 - Dos enemigos BALLOON4 seguidos desde la izquierda/derecha. Simetricos j = 20; enemy_formation_[j].number_of_enemies = 4; inc_x = Balloon::WIDTH_4 + 1; inc_time = 0; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { Uint8 half = enemy_formation_[j].number_of_enemies / 2; if (i < half) { enemy_formation_[j].init[i].x = X4_0 + (i * inc_x); enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; } else { enemy_formation_[j].init[i].x = X4_100 - ((i - half) * inc_x); enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; } enemy_formation_[j].init[i].y = Y4; enemy_formation_[j].init[i].kind = Balloon::BALLOON_4; enemy_formation_[j].init[i].creation_counter = CREATION_TIME + (inc_time * i); } // #21 - Diez enemigos BALLOON2 uno detras del otro. Izquierda/derecha. Simetricos j = 21; enemy_formation_[j].number_of_enemies = 10; inc_x = Balloon::WIDTH_2 + 1; inc_time = 3; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { Uint8 half = enemy_formation_[j].number_of_enemies / 2; if (i < half) { enemy_formation_[j].init[i].x = X2_0 + (i * inc_x); enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].creation_counter = (CREATION_TIME) - (inc_time * i); } else { enemy_formation_[j].init[i].x = X2_100 - ((i - half) * inc_x); enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].creation_counter = (CREATION_TIME) - (inc_time * (i - half)); } enemy_formation_[j].init[i].y = Y2; enemy_formation_[j].init[i].kind = Balloon::BALLOON_2; } // #22 - Diez enemigos BALLOON3. Hacia la derecha/izquierda. Separados. Simetricos j = 22; enemy_formation_[j].number_of_enemies = 10; inc_x = Balloon::WIDTH_3 * 2; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { Uint8 half = enemy_formation_[j].number_of_enemies / 2; if (i < half) { enemy_formation_[j].init[i].x = X3_0 + (i * inc_x); enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].creation_counter = (CREATION_TIME) - (inc_time * i); } else { enemy_formation_[j].init[i].x = X3_100 - ((i - half) * inc_x); enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].creation_counter = (CREATION_TIME) - (inc_time * (i - half)); } enemy_formation_[j].init[i].y = Y3; enemy_formation_[j].init[i].kind = Balloon::BALLOON_3; } // #23 - Diez enemigos BALLOON3. Hacia la derecha. Juntos. Simetricos j = 23; enemy_formation_[j].number_of_enemies = 10; inc_x = Balloon::WIDTH_3 + 1; inc_time = 10; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { Uint8 half = enemy_formation_[j].number_of_enemies / 2; if (i < half) { enemy_formation_[j].init[i].x = X3_0 + (i * inc_x); enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].creation_counter = (CREATION_TIME) - (inc_time * i); } else { enemy_formation_[j].init[i].x = X3_100 - ((i - half) * inc_x); enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].creation_counter = (CREATION_TIME) - (inc_time * (i - half)); } enemy_formation_[j].init[i].y = Y3; enemy_formation_[j].init[i].kind = Balloon::BALLOON_3; } // #24 - Treinta enemigos BALLOON1. Del centro hacia los extremos. Juntos. Simetricos j = 24; enemy_formation_[j].number_of_enemies = 30; inc_time = 5; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { Uint8 half = enemy_formation_[j].number_of_enemies / 2; if (i < half) { enemy_formation_[j].init[i].x = X1_50; enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].creation_counter = (CREATION_TIME) + (inc_time * i); } else { enemy_formation_[j].init[i].x = X1_50; enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].creation_counter = (CREATION_TIME) + (inc_time * (i - half)); } enemy_formation_[j].init[i].y = Y1; enemy_formation_[j].init[i].kind = Balloon::BALLOON_1; } // #25 - Treinta enemigos BALLOON1. Del centro hacia adentro. Juntos. Simetricos j = 25; enemy_formation_[j].number_of_enemies = 30; inc_time = 5; for (int i = 0; i < enemy_formation_[j].number_of_enemies; i++) { Uint8 half = enemy_formation_[j].number_of_enemies / 2; if (i < half) { enemy_formation_[j].init[i].x = X1_50 + 20; enemy_formation_[j].init[i].vel_x = Balloon::VELX_NEGATIVE; enemy_formation_[j].init[i].creation_counter = (CREATION_TIME) - (inc_time * i); } else { enemy_formation_[j].init[i].x = X1_50 - 20; enemy_formation_[j].init[i].vel_x = Balloon::VELX_POSITIVE; enemy_formation_[j].init[i].creation_counter = (CREATION_TIME) - (inc_time * (i - half)); } enemy_formation_[j].init[i].y = Y1; enemy_formation_[j].init[i].kind = Balloon::BALLOON_1; } } // Duplica las formaciones 0..25 como hexágonos en el rango 50..75, y configura la formación 99 (TEST) void Game::initEnemyFormationsHexagonsAndTest() { constexpr Uint8 LAST_SYMMETRIC = 25; // Crea las mismas formaciones pero con hexagonos a partir de la posición 50 del vector for (int k = 0; k < LAST_SYMMETRIC + 1; k++) { enemy_formation_[k + 50].number_of_enemies = enemy_formation_[k].number_of_enemies; for (int i = 0; i < enemy_formation_[k + 50].number_of_enemies; i++) { enemy_formation_[k + 50].init[i].x = enemy_formation_[k].init[i].x; enemy_formation_[k + 50].init[i].y = enemy_formation_[k].init[i].y; enemy_formation_[k + 50].init[i].vel_x = enemy_formation_[k].init[i].vel_x; enemy_formation_[k + 50].init[i].creation_counter = enemy_formation_[k].init[i].creation_counter; enemy_formation_[k + 50].init[i].kind = enemy_formation_[k].init[i].kind + 4; } } // TEST enemy_formation_[99].number_of_enemies = 4; enemy_formation_[99].init[0].x = 10; enemy_formation_[99].init[0].y = Y1; enemy_formation_[99].init[0].vel_x = 0; enemy_formation_[99].init[0].kind = Balloon::BALLOON_1; enemy_formation_[99].init[0].creation_counter = 200; enemy_formation_[99].init[1].x = 50; enemy_formation_[99].init[1].y = Y1; enemy_formation_[99].init[1].vel_x = 0; enemy_formation_[99].init[1].kind = Balloon::BALLOON_2; enemy_formation_[99].init[1].creation_counter = 200; enemy_formation_[99].init[2].x = 90; enemy_formation_[99].init[2].y = Y1; enemy_formation_[99].init[2].vel_x = 0; enemy_formation_[99].init[2].kind = Balloon::BALLOON_3; enemy_formation_[99].init[2].creation_counter = 200; enemy_formation_[99].init[3].x = 140; enemy_formation_[99].init[3].y = Y1; enemy_formation_[99].init[3].vel_x = 0; enemy_formation_[99].init[3].kind = Balloon::BALLOON_4; enemy_formation_[99].init[3].creation_counter = 200; } // Inicializa los conjuntos de formaciones void Game::initEnemyPools() { // EnemyPool #0 enemy_pool_[0].set[0] = &enemy_formation_[0]; enemy_pool_[0].set[1] = &enemy_formation_[1]; enemy_pool_[0].set[2] = &enemy_formation_[2]; enemy_pool_[0].set[3] = &enemy_formation_[3]; enemy_pool_[0].set[4] = &enemy_formation_[4]; enemy_pool_[0].set[5] = &enemy_formation_[5]; enemy_pool_[0].set[6] = &enemy_formation_[6]; enemy_pool_[0].set[7] = &enemy_formation_[7]; enemy_pool_[0].set[8] = &enemy_formation_[8]; enemy_pool_[0].set[9] = &enemy_formation_[9]; // EnemyPool #1 enemy_pool_[1].set[0] = &enemy_formation_[10]; enemy_pool_[1].set[1] = &enemy_formation_[11]; enemy_pool_[1].set[2] = &enemy_formation_[12]; enemy_pool_[1].set[3] = &enemy_formation_[13]; enemy_pool_[1].set[4] = &enemy_formation_[14]; enemy_pool_[1].set[5] = &enemy_formation_[15]; enemy_pool_[1].set[6] = &enemy_formation_[16]; enemy_pool_[1].set[7] = &enemy_formation_[17]; enemy_pool_[1].set[8] = &enemy_formation_[18]; enemy_pool_[1].set[9] = &enemy_formation_[19]; // EnemyPool #2 enemy_pool_[2].set[0] = &enemy_formation_[0]; enemy_pool_[2].set[1] = &enemy_formation_[1]; enemy_pool_[2].set[2] = &enemy_formation_[2]; enemy_pool_[2].set[3] = &enemy_formation_[3]; enemy_pool_[2].set[4] = &enemy_formation_[4]; enemy_pool_[2].set[5] = &enemy_formation_[55]; enemy_pool_[2].set[6] = &enemy_formation_[56]; enemy_pool_[2].set[7] = &enemy_formation_[57]; enemy_pool_[2].set[8] = &enemy_formation_[58]; enemy_pool_[2].set[9] = &enemy_formation_[59]; // EnemyPool #3 enemy_pool_[3].set[0] = &enemy_formation_[50]; enemy_pool_[3].set[1] = &enemy_formation_[51]; enemy_pool_[3].set[2] = &enemy_formation_[52]; enemy_pool_[3].set[3] = &enemy_formation_[53]; enemy_pool_[3].set[4] = &enemy_formation_[54]; enemy_pool_[3].set[5] = &enemy_formation_[5]; enemy_pool_[3].set[6] = &enemy_formation_[6]; enemy_pool_[3].set[7] = &enemy_formation_[7]; enemy_pool_[3].set[8] = &enemy_formation_[8]; enemy_pool_[3].set[9] = &enemy_formation_[9]; // EnemyPool #4 enemy_pool_[4].set[0] = &enemy_formation_[60]; enemy_pool_[4].set[1] = &enemy_formation_[61]; enemy_pool_[4].set[2] = &enemy_formation_[62]; enemy_pool_[4].set[3] = &enemy_formation_[63]; enemy_pool_[4].set[4] = &enemy_formation_[64]; enemy_pool_[4].set[5] = &enemy_formation_[65]; enemy_pool_[4].set[6] = &enemy_formation_[66]; enemy_pool_[4].set[7] = &enemy_formation_[67]; enemy_pool_[4].set[8] = &enemy_formation_[68]; enemy_pool_[4].set[9] = &enemy_formation_[69]; // EnemyPool #5 enemy_pool_[5].set[0] = &enemy_formation_[10]; enemy_pool_[5].set[1] = &enemy_formation_[61]; enemy_pool_[5].set[2] = &enemy_formation_[12]; enemy_pool_[5].set[3] = &enemy_formation_[63]; enemy_pool_[5].set[4] = &enemy_formation_[14]; enemy_pool_[5].set[5] = &enemy_formation_[65]; enemy_pool_[5].set[6] = &enemy_formation_[16]; enemy_pool_[5].set[7] = &enemy_formation_[67]; enemy_pool_[5].set[8] = &enemy_formation_[18]; enemy_pool_[5].set[9] = &enemy_formation_[69]; // EnemyPool #6 enemy_pool_[6].set[0] = &enemy_formation_[60]; enemy_pool_[6].set[1] = &enemy_formation_[11]; enemy_pool_[6].set[2] = &enemy_formation_[62]; enemy_pool_[6].set[3] = &enemy_formation_[13]; enemy_pool_[6].set[4] = &enemy_formation_[64]; enemy_pool_[6].set[5] = &enemy_formation_[15]; enemy_pool_[6].set[6] = &enemy_formation_[66]; enemy_pool_[6].set[7] = &enemy_formation_[17]; enemy_pool_[6].set[8] = &enemy_formation_[68]; enemy_pool_[6].set[9] = &enemy_formation_[19]; // EnemyPool #7 enemy_pool_[7].set[0] = &enemy_formation_[20]; enemy_pool_[7].set[1] = &enemy_formation_[21]; enemy_pool_[7].set[2] = &enemy_formation_[22]; enemy_pool_[7].set[3] = &enemy_formation_[23]; enemy_pool_[7].set[4] = &enemy_formation_[24]; enemy_pool_[7].set[5] = &enemy_formation_[65]; enemy_pool_[7].set[6] = &enemy_formation_[66]; enemy_pool_[7].set[7] = &enemy_formation_[67]; enemy_pool_[7].set[8] = &enemy_formation_[68]; enemy_pool_[7].set[9] = &enemy_formation_[69]; // EnemyPool #8 enemy_pool_[8].set[0] = &enemy_formation_[70]; enemy_pool_[8].set[1] = &enemy_formation_[71]; enemy_pool_[8].set[2] = &enemy_formation_[72]; enemy_pool_[8].set[3] = &enemy_formation_[73]; enemy_pool_[8].set[4] = &enemy_formation_[74]; enemy_pool_[8].set[5] = &enemy_formation_[15]; enemy_pool_[8].set[6] = &enemy_formation_[16]; enemy_pool_[8].set[7] = &enemy_formation_[17]; enemy_pool_[8].set[8] = &enemy_formation_[18]; enemy_pool_[8].set[9] = &enemy_formation_[19]; // EnemyPool #9 enemy_pool_[9].set[0] = &enemy_formation_[20]; enemy_pool_[9].set[1] = &enemy_formation_[21]; enemy_pool_[9].set[2] = &enemy_formation_[22]; enemy_pool_[9].set[3] = &enemy_formation_[23]; enemy_pool_[9].set[4] = &enemy_formation_[24]; enemy_pool_[9].set[5] = &enemy_formation_[70]; enemy_pool_[9].set[6] = &enemy_formation_[71]; enemy_pool_[9].set[7] = &enemy_formation_[72]; enemy_pool_[9].set[8] = &enemy_formation_[73]; enemy_pool_[9].set[9] = &enemy_formation_[74]; } // Inicializa las fases del juego void Game::initGameStages() { // STAGE 1 stage_[0].number = 1; stage_[0].current_power = 0; stage_[0].power_to_complete = 200; stage_[0].min_menace = 7 + (4 * 1); stage_[0].max_menace = 7 + (4 * 3); stage_[0].enemy_pool = &enemy_pool_[0]; // STAGE 2 stage_[1].number = 2; stage_[1].current_power = 0; stage_[1].power_to_complete = 300; stage_[1].min_menace = 7 + (4 * 2); stage_[1].max_menace = 7 + (4 * 4); stage_[1].enemy_pool = &enemy_pool_[1]; // STAGE 3 stage_[2].number = 3; stage_[2].current_power = 0; stage_[2].power_to_complete = 600; stage_[2].min_menace = 7 + (4 * 3); stage_[2].max_menace = 7 + (4 * 5); stage_[2].enemy_pool = &enemy_pool_[2]; // STAGE 4 stage_[3].number = 4; stage_[3].current_power = 0; stage_[3].power_to_complete = 600; stage_[3].min_menace = 7 + (4 * 3); stage_[3].max_menace = 7 + (4 * 5); stage_[3].enemy_pool = &enemy_pool_[3]; // STAGE 5 stage_[4].number = 5; stage_[4].current_power = 0; stage_[4].power_to_complete = 600; stage_[4].min_menace = 7 + (4 * 4); stage_[4].max_menace = 7 + (4 * 6); stage_[4].enemy_pool = &enemy_pool_[4]; // STAGE 6 stage_[5].number = 6; stage_[5].current_power = 0; stage_[5].power_to_complete = 600; stage_[5].min_menace = 7 + (4 * 4); stage_[5].max_menace = 7 + (4 * 6); stage_[5].enemy_pool = &enemy_pool_[5]; // STAGE 7 stage_[6].number = 7; stage_[6].current_power = 0; stage_[6].power_to_complete = 650; stage_[6].min_menace = 7 + (4 * 5); stage_[6].max_menace = 7 + (4 * 7); stage_[6].enemy_pool = &enemy_pool_[6]; // STAGE 8 stage_[7].number = 8; stage_[7].current_power = 0; stage_[7].power_to_complete = 750; stage_[7].min_menace = 7 + (4 * 5); stage_[7].max_menace = 7 + (4 * 7); stage_[7].enemy_pool = &enemy_pool_[7]; // STAGE 9 stage_[8].number = 9; stage_[8].current_power = 0; stage_[8].power_to_complete = 850; stage_[8].min_menace = 7 + (4 * 6); stage_[8].max_menace = 7 + (4 * 8); stage_[8].enemy_pool = &enemy_pool_[8]; // STAGE 10 stage_[9].number = 10; stage_[9].current_power = 0; stage_[9].power_to_complete = 950; stage_[9].min_menace = 7 + (4 * 7); stage_[9].max_menace = 7 + (4 * 10); stage_[9].enemy_pool = &enemy_pool_[9]; } // Crea una formación de enemigos void Game::deployEnemyFormation() { // Solo despliega una formación enemiga si ha pasado cierto tiempo desde la última if (enemy_deploy_counter_ == 0) { // En este punto se decide entre crear una powerball o una formación enemiga if ((rand() % 100 < 15) && (canPowerBallBeCreated())) { // Crea una powerball createPowerBall(); // Da un poco de margen para que se creen mas enemigos enemy_deploy_counter_ = 50; } else { // Decrementa el contador de despliegues enemigos de la PowerBall power_ball_counter_ > 0 ? power_ball_counter_-- : power_ball_counter_ = 0; // Elige una formación enemiga la azar Uint8 set = (rand() % 10); // Evita repetir la ultima formación enemiga desplegada if (set == last_enemy_deploy_) { ++set %= 10; } last_enemy_deploy_ = set; const Uint8 NUM_ENEMIES = stage_[current_stage_].enemy_pool->set[set]->number_of_enemies; for (int i = 0; i < NUM_ENEMIES; ++i) { createBalloon(stage_[current_stage_].enemy_pool->set[set]->init[i].x, stage_[current_stage_].enemy_pool->set[set]->init[i].y, stage_[current_stage_].enemy_pool->set[set]->init[i].kind, stage_[current_stage_].enemy_pool->set[set]->init[i].vel_x, enemy_speed_, stage_[current_stage_].enemy_pool->set[set]->init[i].creation_counter); } enemy_deploy_counter_ = 300; } } } // Aumenta el poder de la fase void Game::increaseStageCurrentPower(Uint8 power) { stage_[current_stage_].current_power += power; } // Establece el valor de la variable void Game::setHiScore(Uint32 score) { hi_score_ = score; } // Actualiza el valor de hiScore en caso necesario void Game::updateHiScore() { // Si la puntuación actual es mayor que la máxima puntuación for (const auto *player : players_) { if (player->getScore() > hi_score_) { // Actualiza la máxima puntuación hi_score_ = player->getScore(); // Almacena la máxima puntuación en el fichero junto con un checksum score_data_file_[0] = hi_score_; score_data_file_[1] = hi_score_ % 43; // Si se supera la máxima puntuación emite sonido if (!hi_score_achieved_) { hi_score_achieved_ = true; Audio::get()->playSound(hi_score_sound_); } } } } // Transforma un valor numérico en una cadena de 6 cifras auto Game::updateScoreText(Uint32 num) -> std::string { if ((num >= 0) && (num <= 9)) { return ("000000" + std::to_string(num)); } if ((num >= 10) && (num <= 99)) { return ("00000" + std::to_string(num)); } if ((num >= 100) && (num <= 999)) { return ("0000" + std::to_string(num)); } if ((num >= 1000) && (num <= 9999)) { return ("000" + std::to_string(num)); } if ((num >= 010000) && (num <= 99999)) { return ("00" + std::to_string(num)); } if ((num >= 100000) && (num <= 999999)) { return ("0" + std::to_string(num)); } if ((num >= 1000000) && (num <= 9999999)) { return (std::to_string(num)); } return (std::to_string(num)); } // Pinta el marcador en pantalla usando un objeto texto void Game::renderScoreBoard() { // Dibuja el fondo del marcador if (difficulty_ == DIFFICULTY_NORMAL) { // Pone el color gris de siempre SDL_SetRenderDrawColor(renderer_, 46, 63, 71, 255); } else { // Pinta el fondo del marcador del color de la dificultad SDL_SetRenderDrawColor(renderer_, difficulty_color_.r, difficulty_color_.g, difficulty_color_.b, 255); } SDL_FRect f_rect = {0, 160, 256, 32}; SDL_RenderFillRect(renderer_, &f_rect); // Dibuja la linea que separa el marcador de la zona de juego SDL_SetRenderDrawColor(renderer_, 13, 26, 43, 255); SDL_RenderLine(renderer_, 0, 160, 255, 160); // Anclas para los elementos const int OFFSET1 = 162; const int OFFSET2 = OFFSET1 + 7; const int OFFSET3 = OFFSET2 + 7; const int OFFSET4 = OFFSET3 + 7; const int OFFSET_LEFT = PLAY_AREA_LEFT + 45; const int OFFSET_RIGHT = PLAY_AREA_RIGHT - 45; // PLAYER1 - SCORE text_scoreboard_->writeCentered(OFFSET_LEFT, OFFSET1, Lang::get()->getText(53)); text_scoreboard_->writeCentered(OFFSET_LEFT, OFFSET2, updateScoreText(players_[0]->getScore())); // PLAYER1 - MULT text_scoreboard_->writeCentered(OFFSET_LEFT, OFFSET3, Lang::get()->getText(55)); text_scoreboard_->writeCentered(OFFSET_LEFT, OFFSET4, std::to_string(players_[0]->getScoreMultiplier()).substr(0, 3)); if (num_players_ == 2) { // PLAYER2 - SCORE text_scoreboard_->writeCentered(OFFSET_RIGHT, OFFSET1, Lang::get()->getText(54)); text_scoreboard_->writeCentered(OFFSET_RIGHT, OFFSET2, updateScoreText(players_[1]->getScore())); // PLAYER2 - MULT text_scoreboard_->writeCentered(OFFSET_RIGHT, OFFSET3, Lang::get()->getText(55)); text_scoreboard_->writeCentered(OFFSET_RIGHT, OFFSET4, std::to_string(players_[1]->getScoreMultiplier()).substr(0, 3)); } else { // PLAYER2 - SCORE text_scoreboard_->writeCentered(OFFSET_RIGHT, OFFSET1, Lang::get()->getText(54)); text_scoreboard_->writeCentered(OFFSET_RIGHT, OFFSET2, "0000000"); // PLAYER2 - MULT text_scoreboard_->writeCentered(OFFSET_RIGHT, OFFSET3, Lang::get()->getText(55)); text_scoreboard_->writeCentered(OFFSET_RIGHT, OFFSET4, "1.0"); } // STAGE text_scoreboard_->writeCentered(PLAY_AREA_CENTER_X, OFFSET1, Lang::get()->getText(57) + std::to_string(stage_[current_stage_].number)); // POWERMETER power_meter_sprite_->setPosY(OFFSET2); power_meter_sprite_->setSpriteClip(0, 0, 40, 7); power_meter_sprite_->render(); const float PERCENT = (stage_[current_stage_].current_power * 40.0F) / stage_[current_stage_].power_to_complete; power_meter_sprite_->setSpriteClip(40, 0, (int)PERCENT, 7); power_meter_sprite_->render(); // HI-SCORE text_scoreboard_->writeCentered(PLAY_AREA_CENTER_X, OFFSET3, Lang::get()->getText(56)); text_scoreboard_->writeCentered(PLAY_AREA_CENTER_X, OFFSET4, hi_score_name_ + updateScoreText(hi_score_)); } // Actualiza las variables del jugador void Game::updatePlayers() { for (auto *player : players_) { player->update(); // Comprueba la colisión entre el jugador y los globos if (checkPlayerBalloonCollision(player)) { if (player->isAlive()) { if (demo_.enabled) { section_->name = SECTION_PROG_TITLE; section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS; } else { killPlayer(player); } } } // Comprueba las colisiones entre el jugador y los items checkPlayerItemCollision(player); } } // Actualiza las variables del jugador (time-based) void Game::updatePlayers(float dt_s) { for (auto *player : players_) { player->update(dt_s); if (checkPlayerBalloonCollision(player)) { if (player->isAlive()) { if (demo_.enabled) { section_->name = SECTION_PROG_TITLE; section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS; } else { killPlayer(player); } } } checkPlayerItemCollision(player); } } // Dibuja a los jugadores void Game::renderPlayers() { for (auto *player : players_) { player->render(); } } // Actualiza las variables de la fase void Game::updateStage() { if (stage_[current_stage_].current_power >= stage_[current_stage_].power_to_complete) { // Cambio de fase current_stage_++; last_stage_reached_ = current_stage_; if (current_stage_ == 10) { // Ha llegado al final el juego game_completed_ = true; // Marca el juego como completado current_stage_ = 9; // Deja el valor dentro de los limites stage_[current_stage_].current_power = 0; // Deja el poder a cero para que no vuelva a entrar en esta condición destroyAllBalloons(); // Destruye a todos los enemigos stage_[current_stage_].current_power = 0; // Vuelve a dejar el poder a cero, por lo que hubiera podido subir al destruir todos lo globos menace_current_ = 255; // Sube el nivel de amenaza para que no cree mas globos for (auto *player : players_) { // Añade un millon de puntos a los jugadores que queden vivos if (player->isAlive()) { player->addScore(1000000); } } updateHiScore(); Audio::get()->stopMusic(); } Audio::get()->playSound(stage_change_sound_); stage_bitmap_counter_ = 0; enemy_speed_ = default_enemy_speed_; setBalloonSpeed(enemy_speed_); effect_.flash = true; effect_.shake = true; } // Incrementa el contador del bitmap que aparece mostrando el cambio de fase if (stage_bitmap_counter_ < STAGE_COUNTER) { stage_bitmap_counter_++; } // Si el juego se ha completado, el bitmap se detiene en el centro de la pantalla if (game_completed_) { stage_bitmap_counter_ = std::min(stage_bitmap_counter_, 100); } } // Actualiza las variables de la fase (time-based) void Game::updateStage(float dt_s) { if (stage_[current_stage_].current_power >= stage_[current_stage_].power_to_complete) { current_stage_++; last_stage_reached_ = current_stage_; if (current_stage_ == 10) { game_completed_ = true; current_stage_ = 9; stage_[current_stage_].current_power = 0; destroyAllBalloons(); stage_[current_stage_].current_power = 0; menace_current_ = 255; for (auto *player : players_) { if (player->isAlive()) { player->addScore(1000000); } } updateHiScore(); Audio::get()->stopMusic(); } Audio::get()->playSound(stage_change_sound_); stage_bitmap_counter_ = 0; stage_bitmap_counter_s_ = 0.0F; enemy_speed_ = default_enemy_speed_; setBalloonSpeed(enemy_speed_); effect_.flash = true; effect_.shake = true; } if (stage_bitmap_counter_s_ < (STAGE_COUNTER / 60.0F)) { stage_bitmap_counter_s_ += dt_s; } stage_bitmap_counter_ = std::min(STAGE_COUNTER, static_cast(stage_bitmap_counter_s_ * 60.0F)); if (game_completed_) { stage_bitmap_counter_ = std::min(stage_bitmap_counter_, 100); } } // Actualiza el estado de muerte void Game::updateDeath() { // Comprueba si todos los jugadores estan muertos bool all_dead = true; for (const auto *player : players_) { all_dead &= (!player->isAlive()); } if (all_dead) { if (death_counter_ > 0) { death_counter_--; if ((death_counter_ == 250) || (death_counter_ == 200) || (death_counter_ == 180) || (death_counter_ == 120) || (death_counter_ == 60)) { // Hace sonar aleatoriamente uno de los 4 sonidos de burbujas if (!demo_.enabled) { const Uint8 INDEX = rand() % 4; Ja::Sound *sound[4] = {bubble1_sound_, bubble2_sound_, bubble3_sound_, bubble4_sound_}; Audio::get()->playSound(sound[INDEX]); } } } else { section_->subsection = SUBSECTION_GAME_GAMEOVER; } } } // Actualiza el estado de muerte (time-based). Detecta el creuament dels llindars // 250/200/180/120/60 (en frames) per a reproduir els bubbles als mateixos moments. void Game::updateDeath(float dt_s) { bool all_dead = true; for (const auto *player : players_) { all_dead &= (!player->isAlive()); } if (!all_dead) { return; } if (death_counter_s_ <= 0.0F) { section_->subsection = SUBSECTION_GAME_GAMEOVER; return; } const float PREV_S = death_counter_s_; death_counter_s_ = std::max(0.0F, death_counter_s_ - dt_s); death_counter_ = static_cast(death_counter_s_ * 60.0F); auto crossed = [&](float threshold_frames) { const float TS = threshold_frames / 60.0F; return (PREV_S > TS) && (death_counter_s_ <= TS); }; if (crossed(250.0F) || crossed(200.0F) || crossed(180.0F) || crossed(120.0F) || crossed(60.0F)) { if (!demo_.enabled) { const Uint8 INDEX = rand() % 4; Ja::Sound *sound[4] = {bubble1_sound_, bubble2_sound_, bubble3_sound_, bubble4_sound_}; Audio::get()->playSound(sound[INDEX]); } } } // Renderiza el fade final cuando se acaba la partida void Game::renderDeathFade(int counter) { // Counter debe ir de 0 a 150 SDL_SetRenderDrawColor(renderer_, 0x27, 0x27, 0x36, 255); if (counter < 150) { // 192 / 6 = 32, 6 cuadrados de 32 pixeles SDL_FRect rect[12]; auto h = (float)(counter / 3); for (int i = 0; i < 12; ++i) { rect[i].x = 0; rect[i].y = (float)(i * 16); rect[i].w = (float)GAMECANVAS_WIDTH; if (i == 0) { rect[i].h = h; } else { rect[i].h = std::max(rect[i - 1].h - 3.0F, 0.0F); } SDL_RenderFillRect(renderer_, &rect[i]); } } else { SDL_FRect rect = {0, 0, (float)GAMECANVAS_WIDTH, (float)GAMECANVAS_HEIGHT}; SDL_RenderFillRect(renderer_, &rect); } } // Actualiza los globos void Game::updateBalloons() { for (auto *balloon : balloons_) { balloon->update(); } } // Actualiza los globos (time-based) void Game::updateBalloons(float dt_s) { for (auto *balloon : balloons_) { balloon->update(dt_s); } } // Pinta en pantalla todos los globos activos void Game::renderBalloons() { for (auto *balloon : balloons_) { balloon->render(); } } // Crea un globo nuevo en el vector de globos auto Game::createBalloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 creationtimer) -> Uint8 { const int INDEX = (kind - 1) % 4; auto *b = new Balloon(x, y, kind, velx, speed, creationtimer, balloon_textures_[INDEX], balloon_animations_[INDEX], renderer_); balloons_.push_back(b); return (Uint8)(balloons_.size() - 1); } // Crea una PowerBall void Game::createPowerBall() { const int POS_Y = PLAY_AREA_TOP; const int LEFT = PLAY_AREA_LEFT; const int CENTER = PLAY_AREA_CENTER_X - (Balloon::WIDTH_4 / 2); const int RIGHT = PLAY_AREA_RIGHT - Balloon::WIDTH_4; const int LUCK = rand() % 3; const int X[3] = {LEFT, CENTER, RIGHT}; const float VX[3] = {Balloon::VELX_POSITIVE, Balloon::VELX_POSITIVE, Balloon::VELX_NEGATIVE}; auto *b = new Balloon(X[LUCK], POS_Y, Balloon::POWER_BALL, VX[LUCK], enemy_speed_, 100, balloon_textures_[3], balloon_animations_[3], renderer_); balloons_.push_back(b); power_ball_enabled_ = true; power_ball_counter_ = Balloon::POWERBALL_COUNTER; } // Establece la velocidad de los globos void Game::setBalloonSpeed(float speed) { for (auto *balloon : balloons_) { if (balloon->isEnabled()) { balloon->setSpeed(speed); } } } // Incrementa la velocidad de los globos void Game::incBalloonSpeed() { // La velocidad solo se incrementa en el modo normal if (difficulty_ == DIFFICULTY_NORMAL) { if (enemy_speed_ == Balloon::SPEED_1) { enemy_speed_ = Balloon::SPEED_2; } else if (enemy_speed_ == Balloon::SPEED_2) { enemy_speed_ = Balloon::SPEED_3; } else if (enemy_speed_ == Balloon::SPEED_3) { enemy_speed_ = Balloon::SPEED_4; } else if (enemy_speed_ == Balloon::SPEED_4) { enemy_speed_ = Balloon::SPEED_5; } setBalloonSpeed(enemy_speed_); } } // Actualiza la velocidad de los globos en funcion del poder acumulado de la fase void Game::updateBalloonSpeed() { const float PERCENT = (float)stage_[current_stage_].current_power / (float)stage_[current_stage_].power_to_complete; if (enemy_speed_ == Balloon::SPEED_1) { if (PERCENT > 0.2F) { incBalloonSpeed(); } } else if (enemy_speed_ == Balloon::SPEED_2) { if (PERCENT > 0.4F) { incBalloonSpeed(); } } else if (enemy_speed_ == Balloon::SPEED_3) { if (PERCENT > 0.6F) { incBalloonSpeed(); } } else if (enemy_speed_ == Balloon::SPEED_4) { if (PERCENT > 0.8F) { incBalloonSpeed(); } } } // Explosiona un globo. Lo destruye y crea otros dos si es el caso void Game::popBalloon(Balloon *balloon) { // Aumenta el poder de la fase increaseStageCurrentPower(1); balloons_popped_++; const Uint8 KIND = balloon->getKind(); switch (KIND) { // Tipus més petits: simplement elimina el globó case Balloon::BALLOON_1: case Balloon::HEXAGON_1: balloon->pop(); break; // Si es del tipo PowerBall, destruye todos los globos case Balloon::POWER_BALL: destroyAllBalloons(); power_ball_enabled_ = false; enemy_deploy_counter_ = 20; break; // En cualquier otro caso, crea dos globos de un tipo inferior default: const int INDEX = createBalloon(0, balloon->getPosY(), balloon->getKind() - 1, Balloon::VELX_NEGATIVE, enemy_speed_, 0); balloons_[INDEX]->allignTo(balloon->getPosX() + (balloon->getWidth() / 2)); if (balloons_[INDEX]->getClass() == Balloon::BALLOON_CLASS) { balloons_[INDEX]->setVelY(-2.50F); } else { balloons_[INDEX]->setVelY(Balloon::VELX_NEGATIVE); } const int INDEX2 = createBalloon(0, balloon->getPosY(), balloon->getKind() - 1, Balloon::VELX_POSITIVE, enemy_speed_, 0); balloons_[INDEX2]->allignTo(balloon->getPosX() + (balloon->getWidth() / 2)); if (balloons_[INDEX2]->getClass() == Balloon::BALLOON_CLASS) { balloons_[INDEX2]->setVelY(-2.50F); } else { balloons_[INDEX2]->setVelY(Balloon::VELX_NEGATIVE); } // Elimina el globo balloon->pop(); break; } // Recalcula el nivel de amenaza evaluateAndSetMenace(); } // Explosiona un globo. Lo destruye void Game::destroyBalloon(Balloon *balloon) { int score = 0; Uint8 power = 0; // Calcula la puntuación y el poder que generaria el globo en caso de romperlo a él y a sus hijos switch (balloon->getSize()) { case Balloon::SIZE_4: score = Balloon::SCORE_4 + (2 * Balloon::SCORE_3) + (4 * Balloon::SCORE_2) + (8 * Balloon::SCORE_1); power = 15; break; case Balloon::SIZE_3: score = Balloon::SCORE_3 + (2 * Balloon::SCORE_2) + (4 * Balloon::SCORE_1); power = 7; break; case Balloon::SIZE_2: score = Balloon::SCORE_2 + (2 * Balloon::SCORE_1); power = 3; break; case Balloon::SIZE_1: score = Balloon::SCORE_1; power = 1; break; default: score = 0; power = 0; break; } // Otorga los puntos correspondientes al globo for (auto *player : players_) { player->addScore(Uint32(score * player->getScoreMultiplier() * difficulty_score_multiplier_)); } updateHiScore(); // Aumenta el poder de la fase increaseStageCurrentPower(power); balloons_popped_ += power; // Destruye el globo balloon->pop(); // Recalcula el nivel de amenaza evaluateAndSetMenace(); } // Destruye todos los globos void Game::destroyAllBalloons() { for (auto *balloon : balloons_) { if ((balloon->isEnabled()) && (!balloon->isPopping())) { destroyBalloon(balloon); } } enemy_deploy_counter_ = 255; if (!demo_.enabled) { Audio::get()->playSound(power_ball_sound_); } effect_.flash = true; effect_.shake = true; } // Detiene todos los globos void Game::stopAllBalloons(Uint16 time) { for (auto *balloon : balloons_) { if (balloon->isEnabled()) { balloon->setStop(true); balloon->setStoppedTimer(time); } } } // Pone en marcha todos los globos void Game::startAllBalloons() { for (auto *balloon : balloons_) { if ((balloon->isEnabled()) && (!balloon->isBeingCreated())) { balloon->setStop(false); balloon->setStoppedTimer(0); } } } // Vacia el vector de globos void Game::freeBalloons() { if (!balloons_.empty()) { for (int i = balloons_.size() - 1; i >= 0; --i) { if (!balloons_[i]->isEnabled()) { delete balloons_[i]; balloons_.erase(balloons_.begin() + i); } } } } // Comprueba la colisión entre el jugador y los globos activos auto Game::checkPlayerBalloonCollision(Player *player) -> bool { for (auto *balloon : balloons_) { if ((balloon->isEnabled()) && !(balloon->isStopped()) && !(balloon->isInvulnerable())) { if (checkCollision(player->getCollider(), balloon->getCollider())) { return true; } } } return false; } // Comprueba la colisión entre el jugador y los items void Game::checkPlayerItemCollision(Player *player) { if (!player->isAlive()) { return; } for (auto *item : items_) { if (item->isEnabled()) { if (checkCollision(player->getCollider(), item->getCollider())) { switch (item->getId()) { case Item::Id::DISK: player->addScore(1000); updateHiScore(); createItemScoreSprite(item->getPosX() + (item->getWidth() / 2) - (n1000_sprite_->getWidth() / 2), player->getPosY(), n1000_sprite_); Audio::get()->playSound(item_pick_up_sound_); break; case Item::Id::GAVINA: player->addScore(2500); updateHiScore(); createItemScoreSprite(item->getPosX() + (item->getWidth() / 2) - (n2500_sprite_->getWidth() / 2), player->getPosY(), n2500_sprite_); Audio::get()->playSound(item_pick_up_sound_); break; case Item::Id::PACMAR: player->addScore(5000); updateHiScore(); createItemScoreSprite(item->getPosX() + (item->getWidth() / 2) - (n5000_sprite_->getWidth() / 2), player->getPosY(), n5000_sprite_); Audio::get()->playSound(item_pick_up_sound_); break; case Item::Id::CLOCK: enableTimeStopItem(); Audio::get()->playSound(item_pick_up_sound_); break; case Item::Id::COFFEE: if (player->getCoffees() == 2) { player->addScore(5000); updateHiScore(); createItemScoreSprite(item->getPosX() + (item->getWidth() / 2) - (n5000_sprite_->getWidth() / 2), player->getPosY(), n5000_sprite_); } player->giveExtraHit(); Audio::get()->playSound(item_pick_up_sound_); break; case Item::Id::COFFEE_MACHINE: player->setPowerUp(true); Audio::get()->playSound(item_pick_up_sound_); coffee_machine_enabled_ = false; break; default: break; } item->disable(); } } } } // Comprueba y procesa la colisión entre las balas y los globos void Game::checkBulletBalloonCollision() { for (auto *bullet : bullets_) { for (auto *balloon : balloons_) { if (!balloon->isEnabled() || balloon->isInvulnerable() || !bullet->isEnabled()) { continue; } if (checkCollision(balloon->getCollider(), bullet->getCollider())) { resolveBulletBalloonHit(bullet, balloon); break; } } } } // Resuelve un impacto bala-globo: puntos, sonido, explosión, drop de item void Game::resolveBulletBalloonHit(Bullet *bullet, Balloon *balloon) { // Otorga los puntos al jugador que disparó la bala const int INDEX = bullet->getOwner(); players_[INDEX]->incScoreMultiplier(); players_[INDEX]->addScore(Uint32(balloon->getScore() * players_[INDEX]->getScoreMultiplier() * difficulty_score_multiplier_)); updateHiScore(); popBalloon(balloon); if (!demo_.enabled) { Audio::get()->playSound(balloon_sound_); } bullet->disable(); // Suelta el item en caso de que salga uno const Item::Id DROPPED_ITEM = dropItem(); if ((DROPPED_ITEM == Item::Id::NONE) || demo_.enabled || demo_.recording) { return; } if (DROPPED_ITEM != Item::Id::COFFEE_MACHINE) { createItem(DROPPED_ITEM, balloon->getPosX(), balloon->getPosY()); Audio::get()->playSound(item_drop_sound_); } else { createItem(DROPPED_ITEM, players_[INDEX]->getPosX(), 0); coffee_machine_enabled_ = true; } } // Mueve las balas activas void Game::moveBullets() { for (auto *bullet : bullets_) { if (bullet->isEnabled()) { if (bullet->move() == Bullet::MoveResult::OUT) { players_[bullet->getOwner()]->decScoreMultiplier(); } } } } // Mueve las balas activas (time-based) void Game::moveBullets(float dt_s) { for (auto *bullet : bullets_) { if (bullet->isEnabled()) { if (bullet->move(dt_s) == Bullet::MoveResult::OUT) { players_[bullet->getOwner()]->decScoreMultiplier(); } } } } // Pinta las balas activas void Game::renderBullets() { for (auto *bullet : bullets_) { if (bullet->isEnabled()) { bullet->render(); } } } // Crea un objeto bala void Game::createBullet(int x, int y, Bullet::Kind kind, bool powered_up, int owner) { auto *b = new Bullet(x, y, kind, powered_up, owner, bullet_texture_, renderer_); bullets_.push_back(b); } // Vacia el vector de balas void Game::freeBullets() { if (!bullets_.empty()) { for (int i = bullets_.size() - 1; i >= 0; --i) { if (!bullets_[i]->isEnabled()) { delete bullets_[i]; bullets_.erase(bullets_.begin() + i); } } } } // Actualiza los items void Game::updateItems() { for (auto *item : items_) { if (item->isEnabled()) { item->update(); if (item->isOnFloor()) { Audio::get()->playSound(coffee_machine_sound_); effect_.shake = true; } } } } // Actualiza los items (time-based) void Game::updateItems(float dt_s) { for (auto *item : items_) { if (item->isEnabled()) { item->update(dt_s); if (item->isOnFloor()) { Audio::get()->playSound(coffee_machine_sound_); effect_.shake = true; } } } } // Pinta los items activos void Game::renderItems() { for (auto *item : items_) { item->render(); } } // Devuelve un item en función del azar auto Game::dropItem() -> Item::Id { const Uint8 LUCKY_NUMBER = rand() % 100; const Uint8 ITEM = rand() % 6; switch (ITEM) { case 0: if (LUCKY_NUMBER < helper_.item_disk_odds) { return Item::Id::DISK; } break; case 1: if (LUCKY_NUMBER < helper_.item_gavina_odds) { return Item::Id::GAVINA; } break; case 2: if (LUCKY_NUMBER < helper_.item_paco_odds) { return Item::Id::PACMAR; } break; case 3: if (LUCKY_NUMBER < helper_.item_clock_odds) { return Item::Id::CLOCK; } break; case 4: if (LUCKY_NUMBER < helper_.item_coffee_odds) { helper_.item_coffee_odds = ITEM_COFFEE_ODDS; return Item::Id::COFFEE; } else { if (helper_.need_coffee) { helper_.item_coffee_odds++; } } break; case 5: if (LUCKY_NUMBER < helper_.item_coffee_machine_odds) { helper_.item_coffee_machine_odds = ITEM_COFFEE_MACHINE_ODDS; if ((!coffee_machine_enabled_) && (helper_.need_coffee_machine)) { return Item::Id::COFFEE_MACHINE; } } else { if (helper_.need_coffee_machine) { helper_.item_coffee_machine_odds++; } } break; default: break; } return Item::Id::NONE; } // Crea un objeto item void Game::createItem(Item::Id kind, float x, float y) { const auto INDEX = static_cast(kind) - 1; Item *item = new Item(kind, x, y, item_textures_[INDEX], item_animations_[INDEX], renderer_); items_.push_back(item); } // Vacia el vector de items void Game::freeItems() { if (!items_.empty()) { for (int i = items_.size() - 1; i >= 0; --i) { if (!items_[i]->isEnabled()) { delete items_[i]; items_.erase(items_.begin() + i); } } } } // Crea un objeto SmartSprite para mostrar la puntuación al coger un objeto void Game::createItemScoreSprite(int x, int y, const SmartSprite *sprite) { auto *ss = new SmartSprite(nullptr, renderer_); smart_sprites_.push_back(ss); // Crea una copia del objeto *ss = *sprite; ss->setPosX(x); ss->setPosY(y); ss->setDestX(x); ss->setDestY(y - 15); ss->setEnabled(true); ss->setEnabledCounter(100); } // Vacia el vector de smartsprites void Game::freeSmartSprites() { if (!smart_sprites_.empty()) { for (int i = smart_sprites_.size() - 1; i >= 0; --i) { if (smart_sprites_[i]->hasFinished()) { delete smart_sprites_[i]; smart_sprites_.erase(smart_sprites_.begin() + i); } } } } // Dibuja el efecto de flash void Game::renderFlashEffect() { if (effect_.flash) { // Pantallazo blanco SDL_SetRenderDrawColor(renderer_, 0xFF, 0xFF, 0xFF, 0xFF); SDL_RenderClear(renderer_); effect_.flash = false; } } // Actualiza el efecto de agitar la pantalla void Game::updateShakeEffect() { if (effect_.shake) { if (effect_.shake_counter > 0) { effect_.shake_counter--; } else { effect_.shake = false; effect_.shake_counter = SHAKE_COUNTER; } } } // Actualiza el efecto de agitar la pantalla (time-based). Decrementa // `shake_counter` a cadència fixa de 60Hz amb un acumulador de fase // (independent del framerate) — el render de `updateBackground` segueix // llegint la paritat del counter per fer vibrar els edificis. void Game::updateShakeEffect(float dt_s) { if (!effect_.shake) { shake_phase_s_ = 0.0F; return; } constexpr float STEP_S = 1.0F / 60.0F; shake_phase_s_ += dt_s; while (shake_phase_s_ >= STEP_S) { shake_phase_s_ -= STEP_S; if (effect_.shake_counter > 0) { effect_.shake_counter--; } else { effect_.shake = false; effect_.shake_counter = SHAKE_COUNTER; shake_phase_s_ = 0.0F; break; } } } // Crea un SmartSprite para arrojar el item café al recibir un impacto void Game::throwCoffee(int x, int y) { auto *ss = new SmartSprite(item_textures_[4], renderer_); smart_sprites_.push_back(ss); ss->setPosX(x - 8); ss->setPosY(y - 8); ss->setWidth(16); ss->setHeight(16); // Conversió a px/s i px/s² (era -1..1 px/frame i 0.2 px/frame²) const float VX_PX_PER_S = (-1.0F + (static_cast(rand() % 5) * 0.5F)) * 60.0F; ss->setVelX(VX_PX_PER_S); ss->setVelY(-240.0F); ss->setAccelX(0.0F); ss->setAccelY(720.0F); ss->setDestX(x + (ss->getVelX() * 50)); ss->setDestY(GAMECANVAS_HEIGHT + 1); ss->setEnabled(true); ss->setEnabledCounter(1); ss->setSpriteClip(0, 0, 16, 16); ss->setRotate(true); ss->setRotateSpeed(10); ss->setRotateAmount(90.0); } // Actualiza los SmartSprites void Game::updateSmartSprites() { for (auto *ss : smart_sprites_) { ss->update(); } } // Actualiza los SmartSprites (time-based) void Game::updateSmartSprites(float dt_s) { for (auto *ss : smart_sprites_) { ss->update(dt_s); } } // Pinta los SmartSprites activos void Game::renderSmartSprites() { for (auto *ss : smart_sprites_) { ss->render(); } } // Acciones a realizar cuando el jugador muere void Game::killPlayer(Player *player) { if (!player->isInvulnerable()) { if (player->hasExtraHit()) { player->removeExtraHit(); throwCoffee(player->getPosX() + (player->getWidth() / 2), player->getPosY() + (player->getHeight() / 2)); if (!demo_.enabled) { Audio::get()->playSound(coffee_out_sound_); } } else if (death_sequence_.phase == DeathPhase::NONE) { if (!demo_.enabled) { Audio::get()->pauseMusic(); Audio::get()->playSound(player_collision_sound_); } stopAllBalloons(10); shakeScreen(); death_sequence_.phase = DeathPhase::SHAKING; death_sequence_.phase_start_ticks = SDL_GetTicks(); death_sequence_.player = player; } } } // Actualiza la secuencia de muerte del jugador void Game::updateDeathSequence() { switch (death_sequence_.phase) { case DeathPhase::NONE: case DeathPhase::DONE: break; case DeathPhase::SHAKING: // Espera a que termine el efecto de agitación if (!isDeathShaking()) { death_sequence_.phase = DeathPhase::WAITING; death_sequence_.phase_start_ticks = SDL_GetTicks(); } break; case DeathPhase::WAITING: // Espera 500ms antes de completar la muerte if (SDL_GetTicks() - death_sequence_.phase_start_ticks >= 500) { if (!demo_.enabled) { Audio::get()->playSound(coffee_out_sound_); if (allPlayersAreDead()) { Audio::get()->stopMusic(); } else { Audio::get()->resumeMusic(); } } death_sequence_.player->setAlive(false); death_sequence_.phase = DeathPhase::DONE; death_sequence_.player = nullptr; } break; } } // Calcula y establece el valor de amenaza en funcion de los globos activos void Game::evaluateAndSetMenace() { menace_current_ = std::accumulate(balloons_.begin(), balloons_.end(), Uint8(0), [](Uint8 acc, const Balloon *b) { return b->isEnabled() ? acc + b->getMenace() : acc; }); } // Obtiene el valor de la variable auto Game::getMenace() const -> Uint8 { return menace_current_; } // Establece el valor de la variable void Game::setTimeStopped(bool value) { time_stopped_ = value; } // Obtiene el valor de la variable auto Game::isTimeStopped() const -> bool { return time_stopped_; } // Establece el valor de la variable void Game::setTimeStoppedCounter(Uint16 value) { time_stopped_counter_ = value; time_stopped_counter_s_ = static_cast(value) / 60.0F; } // Incrementa el valor de la variable void Game::incTimeStoppedCounter(Uint16 value) { time_stopped_counter_ += value; time_stopped_counter_s_ += static_cast(value) / 60.0F; } // Actualiza y comprueba el valor de la variable void Game::updateTimeStoppedCounter() { if (isTimeStopped()) { if (time_stopped_counter_ > 0) { time_stopped_counter_--; stopAllBalloons(TIME_STOPPED_COUNTER); } else { disableTimeStopItem(); } } } // Actualiza y comprueba el valor de la variable (time-based) void Game::updateTimeStoppedCounter(float dt_s) { if (!isTimeStopped()) { return; } if (time_stopped_counter_s_ > 0.0F) { time_stopped_counter_s_ = std::max(0.0F, time_stopped_counter_s_ - dt_s); time_stopped_counter_ = static_cast(time_stopped_counter_s_ * 60.0F); stopAllBalloons(TIME_STOPPED_COUNTER); } else { disableTimeStopItem(); } } // Actualiza la variable enemyDeployCounter void Game::updateEnemyDeployCounter() { if (enemy_deploy_counter_ > 0) { enemy_deploy_counter_--; } } // Actualiza enemy_deploy_counter_ (time-based). El comptador es decrementa // a cadència fixa de 60Hz amb un acumulador de fase: és un comptador // discret consultat per `canPowerBallBeCreated()` i altres. void Game::updateEnemyDeployCounter(float dt_s) { if (enemy_deploy_counter_ <= 0) { return; } constexpr float STEP_S = 1.0F / 60.0F; enemy_deploy_phase_s_ += dt_s; while (enemy_deploy_phase_s_ >= STEP_S && enemy_deploy_counter_ > 0) { enemy_deploy_phase_s_ -= STEP_S; enemy_deploy_counter_--; } } // Actualiza el juego void Game::update() { // Actualiza el audio Audio::update(); // Actualiza los efectos basados en tiempo real (no en el throttle del juego) updateDeathShake(); updateDeathSequence(); // Durante la secuencia de muerte, congela el resto del juego if (death_sequence_.phase == DeathPhase::SHAKING || death_sequence_.phase == DeathPhase::WAITING) { return; } // Comprueba que la diferencia de ticks sea mayor a la velocidad del juego if (SDL_GetTicks() - ticks_ > ticks_speed_) { // Actualiza el contador de ticks ticks_ = SDL_GetTicks(); // Actualiza el contador de juego counter_++; // Comprueba el teclado/mando checkGameInput(); // Actualiza las variables del jugador updatePlayers(); // Actualiza el fondo updateBackground(); // Mueve los globos updateBalloons(); // Mueve las balas moveBullets(); // Actualiza los items updateItems(); // Actualiza el valor de currentStage updateStage(); // Actualiza el estado de muerte updateDeath(); // Actualiza los SmartSprites updateSmartSprites(); // Actualiza los contadores de estado y efectos updateTimeStoppedCounter(); updateEnemyDeployCounter(); updateShakeEffect(); // Actualiza el ayudante updateHelper(); // Comprueba las colisiones entre globos y balas checkBulletBalloonCollision(); // Comprueba el nivel de amenaza para ver si se han de crear nuevos enemigos updateMenace(); // Actualiza la velocidad de los enemigos updateBalloonSpeed(); // Actualiza el tramo final de juego, una vez completado updateGameCompleted(); // Vacia los vectores freeBullets(); freeBalloons(); freeItems(); freeSmartSprites(); } } // Actualiza el juego (time-based). Sense el gate del SDL_GetTicks: la cadència // la dicta dt_s, propagat des de iterate() via DeltaTime::tick(). El comptador // global `counter_` queda derivat de `elapsed_s_*60` perquè els lectors // existents (render de l'herba, paths del get_ready, etc.) segueixin valuant. void Game::update(float dt_s) { Audio::update(); updateDeathShake(); updateDeathSequence(); if (death_sequence_.phase == DeathPhase::SHAKING || death_sequence_.phase == DeathPhase::WAITING) { return; } // Acumulador i derivació del comptador legacy elapsed_s_ += dt_s; counter_ = static_cast(elapsed_s_ * 60.0F); checkGameInput(); updatePlayers(dt_s); updateBackground(dt_s); updateBalloons(dt_s); moveBullets(dt_s); updateItems(dt_s); updateStage(dt_s); updateDeath(dt_s); updateSmartSprites(dt_s); updateTimeStoppedCounter(dt_s); updateEnemyDeployCounter(dt_s); updateShakeEffect(dt_s); updateHelper(dt_s); checkBulletBalloonCollision(); updateMenace(); updateBalloonSpeed(); updateGameCompleted(dt_s); freeBullets(); freeBalloons(); freeItems(); freeSmartSprites(); } // Actualiza el fondo void Game::updateBackground() { if (!game_completed_) { // Si el juego no esta completo, la velocidad de las nubes es igual a los globos explotados clouds_speed_ = balloons_popped_; } else { // Si el juego está completado, se reduce la velocidad de las nubes if (clouds_speed_ > 400) { clouds_speed_ -= 25; } else { clouds_speed_ = 200; } } // Calcula la velocidad en función de los globos explotados y el total de globos a explotar para acabar el juego const float SPEED = (-0.2F) + (-3.00F * ((float)clouds_speed_ / (float)total_power_to_complete_game_)); // Aplica la velocidad calculada a las nubes (px/frame) clouds1_a_->setVelX(SPEED); clouds1_b_->setVelX(SPEED); clouds2_a_->setVelX(SPEED / 2); clouds2_b_->setVelX(SPEED / 2); // Mueve las nubes (frame-based) clouds1_a_->move(); clouds1_b_->move(); clouds2_a_->move(); clouds2_b_->move(); // Calcula el offset de las nubes if (clouds1_a_->getPosX() < -clouds1_a_->getWidth()) { clouds1_a_->setPosX(clouds1_a_->getWidth()); } if (clouds1_b_->getPosX() < -clouds1_b_->getWidth()) { clouds1_b_->setPosX(clouds1_b_->getWidth()); } if (clouds2_a_->getPosX() < -clouds2_a_->getWidth()) { clouds2_a_->setPosX(clouds2_a_->getWidth()); } if (clouds2_b_->getPosX() < -clouds2_b_->getWidth()) { clouds2_b_->setPosX(clouds2_b_->getWidth()); } // Calcula el frame de la hierba grass_sprite_->setSpriteClip(0, (6 * (counter_ / 20 % 2)), 256, 6); // Mueve los edificios en funcion de si está activo el efecto de agitarlos if (death_shake_.active) { const int V[] = {-1, 1, -1, 1, -1, 1, -1, 0}; buildings_sprite_->setPosX(V[death_shake_.step]); } else if (effect_.shake) { buildings_sprite_->setPosX(((effect_.shake_counter % 2) * 2) - 1); } else { buildings_sprite_->setPosX(0); } } // Actualiza el fondo (time-based). Velocitats dels núvols expressades com a // px/frame (la conversió a px/s la fa cloud->move(dt_s) multiplicant per 60 // internament — perquè MovingSprite comparteix vx_ entre frame i time-based, // aquí passem la mateixa "velocitat per frame" * 60). void Game::updateBackground(float dt_s) { if (!game_completed_) { clouds_speed_ = balloons_popped_; } else { if (clouds_speed_ > 400) { clouds_speed_ -= 25; } else { clouds_speed_ = 200; } } // Velocitat per frame (mateixa fórmula); en time-based la passem com a px/s. const float SPEED_PX_PER_FRAME = (-0.2F) + (-3.00F * ((float)clouds_speed_ / (float)total_power_to_complete_game_)); const float SPEED_PX_PER_S = SPEED_PX_PER_FRAME * 60.0F; clouds1_a_->setVelX(SPEED_PX_PER_S); clouds1_b_->setVelX(SPEED_PX_PER_S); clouds2_a_->setVelX(SPEED_PX_PER_S / 2.0F); clouds2_b_->setVelX(SPEED_PX_PER_S / 2.0F); clouds1_a_->move(dt_s); clouds1_b_->move(dt_s); clouds2_a_->move(dt_s); clouds2_b_->move(dt_s); if (clouds1_a_->getPosX() < -clouds1_a_->getWidth()) { clouds1_a_->setPosX(clouds1_a_->getWidth()); } if (clouds1_b_->getPosX() < -clouds1_b_->getWidth()) { clouds1_b_->setPosX(clouds1_b_->getWidth()); } if (clouds2_a_->getPosX() < -clouds2_a_->getWidth()) { clouds2_a_->setPosX(clouds2_a_->getWidth()); } if (clouds2_b_->getPosX() < -clouds2_b_->getWidth()) { clouds2_b_->setPosX(clouds2_b_->getWidth()); } // Herba: `counter_` derivat de `elapsed_s_*60` ja oscil·la a 60Hz. grass_sprite_->setSpriteClip(0, (6 * (counter_ / 20 % 2)), 256, 6); if (death_shake_.active) { const int V[] = {-1, 1, -1, 1, -1, 1, -1, 0}; buildings_sprite_->setPosX(V[death_shake_.step]); } else if (effect_.shake) { buildings_sprite_->setPosX(((effect_.shake_counter % 2) * 2) - 1); } else { buildings_sprite_->setPosX(0); } } // Dibuja el fondo void Game::renderBackground() { const float GRADIENT_NUMBER = std::min(((float)balloons_popped_ / 1250.0F), 3.0F); const float PERCENT = GRADIENT_NUMBER - (int)GRADIENT_NUMBER; const int ALPHA = std::max((255 - (int)(255 * PERCENT)), 0); // Dibuja el gradiente 2 sky_colors_sprite_->setSpriteClip(sky_colors_rect_[((int)GRADIENT_NUMBER + 1) % 4]); game_sky_colors_texture_->setAlpha(255); sky_colors_sprite_->render(); // Dibuja el gradiente 1 con una opacidad cada vez menor sky_colors_sprite_->setSpriteClip(sky_colors_rect_[(int)GRADIENT_NUMBER]); game_sky_colors_texture_->setAlpha(ALPHA); sky_colors_sprite_->render(); // Dibuja las nubes clouds1_a_->render(); clouds1_b_->render(); clouds2_a_->render(); clouds2_b_->render(); // Dinuja los edificios buildings_sprite_->render(); // Dibuja la hierba grass_sprite_->render(); } // Dibuja el juego void Game::render() { // Prepara para empezar a dibujar en la textura de juego Screen::get()->start(); // Limpia la pantalla Screen::get()->clean(BG_COLOR); // Dibuja los objetos renderBackground(); renderBalloons(); renderBullets(); renderMessages(); renderItems(); renderSmartSprites(); renderScoreBoard(); renderPlayers(); if ((death_counter_ <= 150) && !players_[0]->isAlive()) { renderDeathFade(150 - death_counter_); } if ((game_completed_) && (game_completed_counter_ >= GAME_COMPLETED_START_FADE)) { renderDeathFade(game_completed_counter_ - GAME_COMPLETED_START_FADE); } renderFlashEffect(); // Vuelca el contenido del renderizador en pantalla Screen::get()->blit(); } // Gestiona el nivel de amenaza void Game::updateMenace() { if (game_completed_) { return; } const float PERCENT = stage_[current_stage_].current_power / stage_[current_stage_].power_to_complete; const Uint8 DIFFERENCE = stage_[current_stage_].max_menace - stage_[current_stage_].min_menace; // Aumenta el nivel de amenaza en función de la puntuación menace_threshold_ = stage_[current_stage_].min_menace + (DIFFERENCE * PERCENT); // Si el nivel de amenza es inferior al umbral if (menace_current_ < menace_threshold_) { // Crea una formación de enemigos deployEnemyFormation(); // Recalcula el nivel de amenaza con el nuevo globo evaluateAndSetMenace(); } } // Gestiona la entrada durante el juego void Game::checkGameInput() { 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; // Atalls globals (zoom finestra, fullscreen, shaders, presets, ...) GlobalInputs::handle(); if (demo_.enabled) { processDemoInput(); } else { processLiveInput(); } } // Rama de checkGameInput: reproduce el input grabado en data_file void Game::processDemoInput() { const int INDEX = 0; const DemoKeys &keys = demo_.data_file[demo_.counter]; if (keys.left == 1) { players_[INDEX]->setInput(Input::Action::LEFT); } if (keys.right == 1) { players_[INDEX]->setInput(Input::Action::RIGHT); } if (keys.no_input == 1) { players_[INDEX]->setInput(Input::Action::INVALID); } if (keys.fire == 1 && players_[INDEX]->canFire()) { players_[INDEX]->setInput(Input::Action::FIRE_CENTER); createBullet(players_[INDEX]->getPosX() + (players_[INDEX]->getWidth() / 2) - 4, players_[INDEX]->getPosY() + (players_[INDEX]->getHeight() / 2), Bullet::Kind::UP, players_[INDEX]->isPowerUp(), INDEX); players_[INDEX]->setFireCooldown(10); } if (keys.fire_left == 1 && players_[INDEX]->canFire()) { players_[INDEX]->setInput(Input::Action::FIRE_LEFT); createBullet(players_[INDEX]->getPosX() + (players_[INDEX]->getWidth() / 2) - 4, players_[INDEX]->getPosY() + (players_[INDEX]->getHeight() / 2), Bullet::Kind::LEFT, players_[INDEX]->isPowerUp(), INDEX); players_[INDEX]->setFireCooldown(10); } if (keys.fire_right == 1 && players_[INDEX]->canFire()) { players_[INDEX]->setInput(Input::Action::FIRE_RIGHT); createBullet(players_[INDEX]->getPosX() + (players_[INDEX]->getWidth() / 2) - 4, players_[INDEX]->getPosY() + (players_[INDEX]->getHeight() / 2), Bullet::Kind::RIGHT, players_[INDEX]->isPowerUp(), INDEX); players_[INDEX]->setFireCooldown(10); } // Si se pulsa cualquier tecla, se sale del modo 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 void Game::processLiveInput() { int i = 0; for (auto *player : players_) { if (player->isAlive()) { processPlayerLiveInput(player, i); i++; } } } // Cuerpo per-player de processLiveInput void Game::processPlayerLiveInput(Player *player, int i) { auto *input = Input::get(); const auto &device = Options::inputs[i]; // Movimiento izquierda / derecha / nada if (input->checkInput(Input::Action::LEFT, Input::Repeat::ON, device.device_type, device.id)) { player->setInput(Input::Action::LEFT); demo_.keys.left = 1; } else if (input->checkInput(Input::Action::RIGHT, Input::Repeat::ON, device.device_type, device.id)) { player->setInput(Input::Action::RIGHT); demo_.keys.right = 1; } else { player->setInput(Input::Action::INVALID); demo_.keys.no_input = 1; } // Disparo al centro if (input->checkInput(Input::Action::FIRE_CENTER, Input::Repeat::ON, device.device_type, device.id) && player->canFire()) { player->setInput(Input::Action::FIRE_CENTER); createBullet(player->getPosX() + (player->getWidth() / 2) - 4, player->getPosY() + (player->getHeight() / 2), Bullet::Kind::UP, player->isPowerUp(), i); player->setFireCooldown(10); Audio::get()->playSound(bullet_sound_); demo_.keys.fire = 1; } // Disparo a la izquierda if (input->checkInput(Input::Action::FIRE_LEFT, Input::Repeat::ON, device.device_type, device.id) && player->canFire()) { player->setInput(Input::Action::FIRE_LEFT); createBullet(player->getPosX() + (player->getWidth() / 2) - 4, player->getPosY() + (player->getHeight() / 2), Bullet::Kind::LEFT, player->isPowerUp(), i); player->setFireCooldown(10); Audio::get()->playSound(bullet_sound_); demo_.keys.fire_left = 1; } // Disparo a la derecha if (input->checkInput(Input::Action::FIRE_RIGHT, Input::Repeat::ON, device.device_type, device.id) && player->canFire()) { player->setInput(Input::Action::FIRE_RIGHT); createBullet(player->getPosX() + (player->getWidth() / 2) - 4, player->getPosY() + (player->getHeight() / 2), Bullet::Kind::RIGHT, player->isPowerUp(), i); player->setFireCooldown(10); Audio::get()->playSound(bullet_sound_); demo_.keys.fire_right = 1; } // Pausa if (input->checkInput(Input::Action::PAUSE, Input::Repeat::OFF, device.device_type, device.id)) { 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; } demo_.counter++; } else if (demo_.recording) { section_->name = SECTION_PROG_QUIT; } } // Pinta diferentes mensajes en la pantalla void Game::renderMessages() { // GetReady if ((counter_ < STAGE_COUNTER) && (!demo_.enabled)) { text_nokia_big2_->write((int)get_ready_bitmap_path_[counter_], PLAY_AREA_CENTER_Y - 8, Lang::get()->getText(75), -2); } // Time Stopped if (time_stopped_) { if ((time_stopped_counter_ > 100) || (time_stopped_counter_ % 10 > 4)) { text_nokia2_->writeDX(Text::FLAG_CENTER, PLAY_AREA_CENTER_X, PLAY_AREA_FIRST_QUARTER_Y, Lang::get()->getText(36) + std::to_string(time_stopped_counter_ / 10), -1, NO_COLOR, 1, SHADOW_COLOR); } if (time_stopped_counter_ > 100) { if (time_stopped_counter_ % 30 == 0) { Audio::get()->playSound(clock_sound_); } } else { if (time_stopped_counter_ % 15 == 0) { Audio::get()->playSound(clock_sound_); } } } // D E M O if (demo_.enabled) { if (demo_.counter % 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); } } // STAGE NUMBER if (stage_bitmap_counter_ < STAGE_COUNTER) { const int STAGE_NUM = stage_[current_stage_].number; std::string stage_text; if (STAGE_NUM == 10) { // Ultima fase stage_text = Lang::get()->getText(79); } else { // X fases restantes stage_text = std::to_string(11 - stage_[current_stage_].number) + Lang::get()->getText(38); } if (!game_completed_) { // Escribe el numero de fases restantes text_nokia_big2_->writeDX(Text::FLAG_CENTER, PLAY_AREA_CENTER_X, stage_bitmap_path_[stage_bitmap_counter_], stage_text, -2, NO_COLOR, 2, SHADOW_COLOR); } else { // Escribe el texto de juego completado stage_text = Lang::get()->getText(50); text_nokia_big2_->writeDX(Text::FLAG_CENTER, PLAY_AREA_CENTER_X, stage_bitmap_path_[stage_bitmap_counter_], stage_text, -2, NO_COLOR, 1, SHADOW_COLOR); text_nokia2_->writeDX(Text::FLAG_CENTER, PLAY_AREA_CENTER_X, stage_bitmap_path_[stage_bitmap_counter_] + text_nokia_big2_->getCharacterSize() + 2, Lang::get()->getText(76), -1, NO_COLOR, 1, SHADOW_COLOR); } } } // Habilita el efecto del item de detener el tiempo void Game::enableTimeStopItem() { stopAllBalloons(TIME_STOPPED_COUNTER); setTimeStopped(true); incTimeStoppedCounter(TIME_STOPPED_COUNTER); if (Audio::getRealMusicState() == Audio::MusicState::PLAYING) { Audio::get()->pauseMusic(); } } // Deshabilita el efecto del item de detener el tiempo void Game::disableTimeStopItem() { time_stopped_ = false; setTimeStoppedCounter(0); startAllBalloons(); if (Audio::getRealMusicState() == Audio::MusicState::PAUSED) { Audio::get()->resumeMusic(); } } // Inicia el efecto de agitación intensa de la pantalla void Game::shakeScreen() { death_shake_.active = true; death_shake_.step = 0; death_shake_.last_step_ticks = SDL_GetTicks(); } // Actualiza el efecto de agitación intensa void Game::updateDeathShake() { if (!death_shake_.active) { return; } Uint32 now = SDL_GetTicks(); if (now - death_shake_.last_step_ticks >= 50) { death_shake_.last_step_ticks = now; death_shake_.step++; if (death_shake_.step >= 8) { death_shake_.active = false; } } } // Indica si el efecto de agitación intensa está activo auto Game::isDeathShaking() const -> bool { return death_shake_.active; } // Ejecuta un frame del juego void Game::iterate() { // Consum del temps real des de l'última iteració. Sempre s'ha de cridar // perquè el rellotge no acumuli a través de transicions/sub-estats. const float DELTA_TIME_S = DeltaTime::tick(); // En modo demo, ni pause ni game over: torna immediatament al títol if (demo_.enabled && (section_->subsection == SUBSECTION_GAME_PAUSE || section_->subsection == SUBSECTION_GAME_GAMEOVER)) { section_->name = SECTION_PROG_TITLE; section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS; return; } switch (section_->subsection) { case SUBSECTION_GAME_PAUSE: iteratePaused(DELTA_TIME_S); break; case SUBSECTION_GAME_GAMEOVER: iterateGameOver(DELTA_TIME_S); break; case SUBSECTION_GAME_PLAY_1P: case SUBSECTION_GAME_PLAY_2P: iteratePlaying(DELTA_TIME_S); break; default: break; } } // Rama de iterate(): pause void Game::iteratePaused(float dt_s) { if (!pause_initialized_) { enterPausedGame(); } updatePausedGame(dt_s); renderPausedGame(); } // Rama de iterate(): game over void Game::iterateGameOver(float dt_s) { if (!game_over_initialized_) { enterGameOverScreen(); } updateGameOverScreen(dt_s); renderGameOverScreen(); } // Rama de iterate(): joc actiu void Game::iteratePlaying(float dt_s) { // Si veníem de Pause/GameOver, el dt acumulat seria enorme; descarta'l. if (pause_initialized_ || game_over_initialized_) { DeltaTime::reset(); } pause_initialized_ = false; game_over_initialized_ = false; if (Audio::getRealMusicState() == Audio::MusicState::STOPPED && !game_completed_ && !demo_.enabled && players_[0]->isAlive()) { Audio::get()->playMusic(game_music_); } update(dt_s); render(); } // Indica si el juego ha terminado auto Game::hasFinished() const -> bool { return section_->name != SECTION_PROG_GAME; } // Procesa un evento individual void Game::handleEvent(const SDL_Event *event) { // SDL_EVENT_QUIT ya lo maneja Director if (event->type == SDL_EVENT_WINDOW_FOCUS_LOST) { // Solo pausar durante el juego activo (no en demo, game over, ni ya en pausa) if (!demo_.enabled && (section_->subsection == SUBSECTION_GAME_PLAY_1P || section_->subsection == SUBSECTION_GAME_PLAY_2P)) { section_->subsection = SUBSECTION_GAME_PAUSE; } } // Eventos específicos de la pantalla de game over if (section_->subsection == SUBSECTION_GAME_GAMEOVER) { if (event->type == SDL_EVENT_KEY_DOWN && static_cast(event->key.repeat) == 0) { if (game_completed_) { game_over_post_fade_ = 1; fade_->activateFade(); Audio::get()->playSound(item_pick_up_sound_); } } } } // Bucle para el juego void Game::run() { while (!hasFinished()) { iterate(); } } // Actualiza las variables del menu de pausa del juego void Game::updatePausedGame() { if (SDL_GetTicks() - ticks_ <= ticks_speed_) { return; } ticks_ = SDL_GetTicks(); // Atalls globals (zoom finestra, fullscreen, shaders, presets, ...) GlobalInputs::handle(); if (leaving_pause_menu_) { updateLeavingPauseMenu(); } else { updatePauseMenuUI(); } } // Actualiza el menu de pausa (time-based) void Game::updatePausedGame(float dt_s) { GlobalInputs::handle(); if (leaving_pause_menu_) { updateLeavingPauseMenu(dt_s); } else { updatePauseMenuUI(dt_s); } } // Rama de updatePausedGame: cuenta atrás de salida y vuelta al juego void Game::updateLeavingPauseMenu() { if (pause_counter_ > 0) { // El contador está descendiendo const bool A = pause_counter_ == 90; const bool B = pause_counter_ == 60; const bool C = pause_counter_ == 30; if (A || B || C) { Audio::get()->playSound(clock_sound_); } pause_counter_--; return; } // Ha finalizado el contador section_->name = SECTION_PROG_GAME; section_->subsection = num_players_ == 1 ? SUBSECTION_GAME_PLAY_1P : SUBSECTION_GAME_PLAY_2P; if (Audio::getRealMusicState() == Audio::MusicState::PAUSED) { Audio::get()->resumeMusic(); } } // Cuenta atrás de salida (time-based). Decrementa pause_counter_ a 60Hz exactes // amb un acumulador de fase, per a mantenir els sons de rellotge als llindars // originals (90, 60, 30 frames = 1.5s, 1.0s, 0.5s restants). void Game::updateLeavingPauseMenu(float dt_s) { if (pause_counter_ <= 0) { section_->name = SECTION_PROG_GAME; section_->subsection = num_players_ == 1 ? SUBSECTION_GAME_PLAY_1P : SUBSECTION_GAME_PLAY_2P; if (Audio::getRealMusicState() == Audio::MusicState::PAUSED) { Audio::get()->resumeMusic(); } return; } constexpr float STEP_S = 1.0F / 60.0F; pause_counter_phase_s_ += dt_s; while (pause_counter_phase_s_ >= STEP_S && pause_counter_ > 0) { pause_counter_phase_s_ -= STEP_S; if (pause_counter_ == 90 || pause_counter_ == 60 || pause_counter_ == 30) { Audio::get()->playSound(clock_sound_); } pause_counter_--; } } // Lógica del menú de pausa (time-based) void Game::updatePauseMenuUI(float dt_s) { pause_menu_->update(); pause_menu_->checkInput(); if (Input::get()->checkInput(Input::Action::PAUSE, Input::Repeat::OFF)) { leaving_pause_menu_ = true; pause_counter_phase_s_ = 0.0F; if (!Options::gameplay.pause_countdown) { pause_counter_ = 0; } return; } switch (pause_menu_->getItemSelected()) { case 1: leaving_pause_menu_ = true; pause_counter_phase_s_ = 0.0F; if (!Options::gameplay.pause_countdown) { pause_counter_ = 0; } break; case 2: fade_->setFadeType(Fade::Type::CENTER); fade_->activateFade(); break; default: break; } fade_->update(dt_s); if (fade_->hasEnded()) { section_->name = SECTION_PROG_TITLE; section_->subsection = SUBSECTION_TITLE_1; Audio::get()->stopMusic(); } } // Rama de updatePausedGame: lógica del menú de pausa (frame-based) void Game::updatePauseMenuUI() { pause_menu_->update(); pause_menu_->checkInput(); // F12 (Action::PAUSE) també tanca el menú de pausa — mateix comportament // que seleccionar "Continue" / cancel·lar amb BACKSPACE. if (Input::get()->checkInput(Input::Action::PAUSE, Input::Repeat::OFF)) { leaving_pause_menu_ = true; if (!Options::gameplay.pause_countdown) { pause_counter_ = 0; } return; } switch (pause_menu_->getItemSelected()) { case 1: leaving_pause_menu_ = true; if (!Options::gameplay.pause_countdown) { pause_counter_ = 0; // salta el compte enrere de 3 segons } break; case 2: fade_->setFadeType(Fade::Type::CENTER); fade_->activateFade(); break; default: break; } fade_->update(); if (fade_->hasEnded()) { section_->name = SECTION_PROG_TITLE; section_->subsection = SUBSECTION_TITLE_1; Audio::get()->stopMusic(); } } // Dibuja el menu de pausa del juego void Game::renderPausedGame() { // Prepara para empezar a dibujar en la textura de juego Screen::get()->start(); // Limpia la pantalla Screen::get()->clean(BG_COLOR); // Pinta el escenario { renderBackground(); renderBalloons(); renderBullets(); renderMessages(); renderItems(); renderSmartSprites(); renderScoreBoard(); renderPlayers(); if ((death_counter_ <= 150) && !players_[0]->isAlive()) { renderDeathFade(150 - death_counter_); } if ((game_completed_) && (game_completed_counter_ >= GAME_COMPLETED_START_FADE)) { renderDeathFade(game_completed_counter_ - GAME_COMPLETED_START_FADE); } renderFlashEffect(); } if (leaving_pause_menu_) { if (pause_counter_ > 0) { text_nokia_big2_->writeCentered(GAMECANVAS_CENTER_X, PLAY_AREA_FIRST_QUARTER_Y, std::to_string((pause_counter_ / 30) + 1)); } } else { pause_menu_->render(); } fade_->render(); // Vuelca el contenido del renderizador en pantalla Screen::get()->blit(); } // Inicializa el estado de pausa del juego void Game::enterPausedGame() { // Pone en pausa la música if (Audio::getRealMusicState() == Audio::MusicState::PLAYING) { Audio::get()->pauseMusic(); } // Reinicia el menu pause_menu_->reset(); leaving_pause_menu_ = false; // Inicializa variables pause_counter_ = 90; pause_initialized_ = true; } // Actualiza los elementos de la pantalla de game over void Game::updateGameOverScreen() { // Calcula la lógica de los objetos if (SDL_GetTicks() - ticks_ > ticks_speed_) { // Actualiza el contador de ticks ticks_ = SDL_GetTicks(); // Atalls globals (zoom finestra, fullscreen, shaders, presets, ...) GlobalInputs::handle(); // Actualiza la lógica del menu game_over_menu_->update(); // Actualiza el fade fade_->update(); // Si ha terminado el fade, actua segun se haya operado if (fade_->hasEnded()) { switch (game_over_post_fade_) { case 0: // YES section_->name = SECTION_PROG_GAME; deleteAllVectorObjects(); init(); section_->subsection = num_players_ == 1 ? SUBSECTION_GAME_PLAY_1P : SUBSECTION_GAME_PLAY_2P; break; case 1: // NO section_->name = SECTION_PROG_TITLE; section_->subsection = SUBSECTION_TITLE_1; break; default: break; } } // Comprueba las entradas para el menu solo si no esta el juego completo if (!game_completed_) { game_over_menu_->checkInput(); // Comprueba si se ha seleccionado algún item del menú switch (game_over_menu_->getItemSelected()) { case 0: // YES game_over_post_fade_ = 0; fade_->activateFade(); break; case 1: // NO game_over_post_fade_ = 1; fade_->activateFade(); break; default: break; } } } } // Actualiza los elementos de la pantalla de game over (time-based) void Game::updateGameOverScreen(float dt_s) { GlobalInputs::handle(); game_over_menu_->update(); fade_->update(dt_s); if (fade_->hasEnded()) { switch (game_over_post_fade_) { case 0: // YES section_->name = SECTION_PROG_GAME; deleteAllVectorObjects(); init(); section_->subsection = num_players_ == 1 ? SUBSECTION_GAME_PLAY_1P : SUBSECTION_GAME_PLAY_2P; break; case 1: // NO section_->name = SECTION_PROG_TITLE; section_->subsection = SUBSECTION_TITLE_1; break; default: break; } } if (!game_completed_) { game_over_menu_->checkInput(); switch (game_over_menu_->getItemSelected()) { case 0: // YES game_over_post_fade_ = 0; fade_->activateFade(); break; case 1: // NO game_over_post_fade_ = 1; fade_->activateFade(); break; default: break; } } } // Dibuja los elementos de la pantalla de game over void Game::renderGameOverScreen() { // Prepara para empezar a dibujar en la textura de juego Screen::get()->start(); // Limpia la pantalla Screen::get()->clean(BG_COLOR); // Dibujo if (!game_completed_) { // Dibujo de haber perdido la partida game_over_sprite_->render(); } else { // Dinujo de haber completado la partida game_over_end_sprite_->render(); } // Dibuja los objetos if (num_players_ == 1) { // Congratulations!! if (game_completed_) { text_->writeCentered(PLAY_AREA_CENTER_X, PLAY_AREA_CENTER_Y - (BLOCK * 8), Lang::get()->getText(50)); } // Game Over text_big_->writeCentered(PLAY_AREA_CENTER_X, PLAY_AREA_CENTER_Y - (BLOCK * 6), Lang::get()->getText(43)); // Your Score text_->writeCentered(PLAY_AREA_CENTER_X, PLAY_AREA_CENTER_Y - (BLOCK * 3), Lang::get()->getText(44) + std::to_string(players_[0]->getScore())); } else { // Congratulations!! if (game_completed_) { text_->writeCentered(PLAY_AREA_CENTER_X, PLAY_AREA_CENTER_Y - (BLOCK * 9), Lang::get()->getText(50)); } // Game Over text_big_->writeCentered(PLAY_AREA_CENTER_X, PLAY_AREA_CENTER_Y - (BLOCK * 7), Lang::get()->getText(43)); // Player1 Score text_->writeCentered(PLAY_AREA_CENTER_X, PLAY_AREA_CENTER_Y - (BLOCK * 4), Lang::get()->getText(77) + std::to_string(players_[0]->getScore())); // Player2 Score text_->writeCentered(PLAY_AREA_CENTER_X, PLAY_AREA_CENTER_Y - (BLOCK * 2), Lang::get()->getText(78) + std::to_string(players_[1]->getScore())); } // Continue? if (!game_completed_) { // Solo dibuja el menu de continuar en el caso de no haber completado la partida text_->writeCentered(199, PLAY_AREA_CENTER_Y + (BLOCK * 3), Lang::get()->getText(45)); game_over_menu_->render(); } // Pinta el fade fade_->render(); // Vuelca el contenido del renderizador en pantalla Screen::get()->blit(); } // Inicializa el estado de game over void Game::enterGameOverScreen() { // Guarda los puntos saveScoreFile(); // Reinicia el menu game_over_menu_->reset(); game_over_post_fade_ = 0; game_over_initialized_ = true; } // Indica si se puede crear una powerball auto Game::canPowerBallBeCreated() -> bool { return (!power_ball_enabled_) && (calculateScreenPower() > Balloon::POWERBALL_SCREENPOWER_MINIMUM) && (power_ball_counter_ == 0); } // Calcula el poder actual de los globos en pantalla auto Game::calculateScreenPower() -> int { return std::accumulate(balloons_.begin(), balloons_.end(), 0, [](int acc, const Balloon *b) { return b->isEnabled() ? acc + b->getPower() : acc; }); } // Inicializa las variables que contienen puntos de ruta para mover objetos void Game::initPaths() { // Vector con los valores del seno para 360 grados float sin[360]; for (int i = 0; i < 360; ++i) { sin[i] = SDL_sinf((float)i * 3.14F / 180.0F); } // Letrero de STAGE # const int FIRST_PART = STAGE_COUNTER / 4; // 50 const int SECOND_PART = FIRST_PART * 3; // 150 const int CENTER_POINT = PLAY_AREA_CENTER_Y - (BLOCK * 2); const int DISTANCE = (PLAY_AREA_BOTTOM) - (PLAY_AREA_CENTER_Y - 16); for (int i = 0; i < STAGE_COUNTER; ++i) { if (i < FIRST_PART) { stage_bitmap_path_[i] = (sin[(int)((i * 1.8F) + 90)] * (DISTANCE) + CENTER_POINT); } else if (i < SECOND_PART) { stage_bitmap_path_[i] = (int)CENTER_POINT; } else { stage_bitmap_path_[i] = (sin[(int)(((i - 149) * 1.8F) + 90)] * (CENTER_POINT + 17) - 17); } } // Letrero de GetReady const int SIZE = text_nokia_big2_->lenght(Lang::get()->getText(75), -2); const float START1 = PLAY_AREA_LEFT - SIZE; const float FINISH1 = PLAY_AREA_CENTER_X - (SIZE / 2); const float START2 = FINISH1; const float FINISH2 = PLAY_AREA_RIGHT; const float DISTANCE1 = FINISH1 - START1; const float DISTANCE2 = FINISH2 - START2; for (int i = 0; i < STAGE_COUNTER; ++i) { if (i < FIRST_PART) { get_ready_bitmap_path_[i] = sin[(int)(i * 1.8F)]; get_ready_bitmap_path_[i] *= DISTANCE1; get_ready_bitmap_path_[i] -= SIZE; } else if (i < SECOND_PART) { get_ready_bitmap_path_[i] = (int)FINISH1; } else { get_ready_bitmap_path_[i] = sin[(int)((i - 150) * 1.8F)]; get_ready_bitmap_path_[i] *= DISTANCE2; get_ready_bitmap_path_[i] += FINISH1; } } } // Actualiza el tramo final de juego, una vez completado void Game::updateGameCompleted() { if (game_completed_) { game_completed_counter_++; } if (game_completed_counter_ == GAME_COMPLETED_END) { section_->subsection = SUBSECTION_GAME_GAMEOVER; } } // Actualiza el tramo final de juego (time-based) void Game::updateGameCompleted(float dt_s) { if (!game_completed_) { return; } const int PREV = game_completed_counter_; game_completed_counter_s_ += dt_s; game_completed_counter_ = static_cast(game_completed_counter_s_ * 60.0F); if (PREV < GAME_COMPLETED_END && game_completed_counter_ >= GAME_COMPLETED_END) { section_->subsection = SUBSECTION_GAME_GAMEOVER; } } // Actualiza las variables de ayuda void Game::updateHelper() { // Solo ofrece ayuda cuando la amenaza es elevada if (menace_current_ > 15) { for (const auto *player : players_) { helper_.need_coffee = player->getCoffees() == 0; helper_.need_coffee_machine = !player->isPowerUp(); } } else { helper_.need_coffee = false; helper_.need_coffee_machine = false; } } // Actualiza las variables de ayuda (time-based). De moment cap timer real // dins helper_; clonem la lògica per consistència d'API. void Game::updateHelper([[maybe_unused]] float dt_s) { if (menace_current_ > 15) { for (const auto *player : players_) { helper_.need_coffee = player->getCoffees() == 0; helper_.need_coffee_machine = !player->isPowerUp(); } } else { helper_.need_coffee = false; helper_.need_coffee_machine = false; } } // Comprueba si todos los jugadores han muerto auto Game::allPlayersAreDead() -> bool { bool success = true; for (const auto *player : players_) { success &= (!player->isAlive()); } return success; } // Elimina todos los objetos contenidos en vectores void Game::deleteAllVectorObjects() { for (auto *player : players_) { delete player; }; players_.clear(); for (auto *ballon : balloons_) { delete ballon; }; balloons_.clear(); for (auto *bullet : bullets_) { delete bullet; }; bullets_.clear(); for (auto *item : items_) { delete item; }; items_.clear(); for (auto *smart_sprite : smart_sprites_) { delete smart_sprite; }; smart_sprites_.clear(); } // Establece la máxima puntuación desde fichero o desde las puntuaciones online void Game::setHiScore() { // Carga el fichero de puntos loadScoreFile(); hi_score_name_ = ""; }