#include "game.h" #include // Para SDL_GetTicks, SDL_SetRenderTarget #include // Para rand, size_t #include // Para find_if, clamp, find, min #include // Para function #include // Para distance, size #include "asset.h" // Para Asset #include "audio.h" // Para Audio #include "background.h" // Para Background #include "balloon.h" // Para Balloon, BALLOON_SPEED #include "balloon_manager.h" // Para BalloonManager #include "bullet.h" // Para Bullet, BulletType, BulletMoveStatus #include "fade.h" // Para Fade, FadeType, FadeMode #include "global_events.h" // Para check #include "global_inputs.h" // Para check #include "input.h" // Para InputAction, Input, INPUT_DO_NOT_A... #include "item.h" // Para Item, ItemType #include "lang.h" // Para getText #include "manage_hiscore_table.h" // Para HiScoreEntry, ManageHiScoreTable #include "notifier.h" // Para Notifier #include "param.h" // Para Param, param, ParamGame, ParamScor... #include "path_sprite.h" // Para Path, PathSprite, createPath, Path... #include "player.h" // Para Player, PlayerState #include "resource.h" // Para Resource #include "scoreboard.h" // Para Scoreboard, ScoreboardMode, SCOREB... #include "screen.h" // Para Screen #include "section.h" // Para Name, name, AttractMode, Options #include "smart_sprite.h" // Para SmartSprite #include "stage.h" // Para number, get, Stage, total_power #include "tabe.h" // Para Tabe, TabeState #include "text.h" // Para Text #include "texture.h" // Para Texture #include "ui/service_menu.h" // Para ServiceMenu // Constructor Game::Game(int player_id, int current_stage, bool demo) : renderer_(Screen::get()->getRenderer()), screen_(Screen::get()), input_(Input::get()), background_(std::make_unique()), canvas_(SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.play_area.rect.w, param.game.play_area.rect.h)), fade_in_(std::make_unique()), fade_out_(std::make_unique()), balloon_manager_(std::make_unique()), tabe_(std::make_unique()) { // Pasa variables demo_.enabled = demo; // Otras variables Section::name = Section::Name::GAME; Section::options = Section::Options::NONE; Stage::init(); Stage::number = current_stage; // Asigna texturas y animaciones setResources(); // Crea y configura los objetos Scoreboard::init(); scoreboard_ = Scoreboard::get(); fade_in_->setColor(param.fade.color); fade_in_->setPreDuration(demo_.enabled ? 80 : 0); fade_in_->setPostDuration(0); fade_in_->setType(FadeType::RANDOM_SQUARE); fade_in_->setMode(FadeMode::IN); fade_in_->activate(); fade_out_->setColor(param.fade.color); fade_out_->setPostDuration(param.fade.post_duration); fade_out_->setType(FadeType::VENETIAN); background_->setPos(param.game.play_area.rect); balloon_manager_->setBouncingSounds(param.balloon.bouncing_sound); SDL_SetTextureBlendMode(canvas_, SDL_BLENDMODE_BLEND); // Inicializa el resto de variables initPlayers(player_id); initScoreboard(); initDifficultyVars(); initDemo(player_id); initPaths(); setTotalPower(); #ifdef DEBUG // Si se empieza en una fase que no es la primera if (!demo_.enabled) for (int i = 0; i < Stage::number; ++i) { Stage::total_power += Stage::get(i).power_to_complete; } #endif } Game::~Game() { // [Modo JUEGO] Guarda puntuaciones y transita a modo título if (!demo_.enabled) { auto manager = std::make_unique(Options::settings.hi_score_table); manager->saveToFile(Asset::get()->get("score.bin")); Section::attract_mode = Section::AttractMode::TITLE_TO_DEMO; Audio::get()->stopMusic(); } #ifdef RECORDING saveDemoFile(Asset::get()->get("demo1.bin"), demo_.data.at(0)); #endif Scoreboard::destroy(); SDL_DestroyTexture(canvas_); } // Asigna texturas y animaciones void Game::setResources() { // Texturas - Game_text { game_text_textures_.emplace_back(Resource::get()->getTexture("game_text_1000_points")); game_text_textures_.emplace_back(Resource::get()->getTexture("game_text_2500_points")); game_text_textures_.emplace_back(Resource::get()->getTexture("game_text_5000_points")); game_text_textures_.emplace_back(Resource::get()->getTexture("game_text_powerup")); game_text_textures_.emplace_back(Resource::get()->getTexture("game_text_one_hit")); game_text_textures_.emplace_back(Resource::get()->getTexture("game_text_stop")); game_text_textures_.emplace_back(Resource::get()->getTexture("game_text_100000_points")); } // Texturas - Items { item_textures_.emplace_back(Resource::get()->getTexture("item_points1_disk.png")); item_textures_.emplace_back(Resource::get()->getTexture("item_points2_gavina.png")); item_textures_.emplace_back(Resource::get()->getTexture("item_points3_pacmar.png")); item_textures_.emplace_back(Resource::get()->getTexture("item_clock.png")); item_textures_.emplace_back(Resource::get()->getTexture("item_coffee.png")); item_textures_.emplace_back(Resource::get()->getTexture("item_debian.png")); item_textures_.emplace_back(Resource::get()->getTexture("item_coffee_machine.png")); } // Texturas - Player1 { std::vector> player_texture; player_texture.emplace_back(Resource::get()->getTexture("player1.gif")); player_texture.emplace_back(Resource::get()->getTexture("player1_power.png")); player_textures_.push_back(player_texture); } // Texturas - Player2 { std::vector> player_texture; player_texture.emplace_back(Resource::get()->getTexture("player2.gif")); player_texture.emplace_back(Resource::get()->getTexture("player2_power.png")); player_textures_.push_back(player_texture); } // Animaciones -- Jugador { player_animations_.emplace_back(Resource::get()->getAnimation("player.ani")); player_animations_.emplace_back(Resource::get()->getAnimation("player_power.ani")); } // Animaciones -- Items { item_animations_.emplace_back(Resource::get()->getAnimation("item_points1_disk.ani")); item_animations_.emplace_back(Resource::get()->getAnimation("item_points2_gavina.ani")); item_animations_.emplace_back(Resource::get()->getAnimation("item_points3_pacmar.ani")); item_animations_.emplace_back(Resource::get()->getAnimation("item_clock.ani")); item_animations_.emplace_back(Resource::get()->getAnimation("item_coffee.ani")); item_animations_.emplace_back(Resource::get()->getAnimation("item_debian.ani")); item_animations_.emplace_back(Resource::get()->getAnimation("item_coffee_machine.ani")); } } // Actualiza el valor de hiScore en caso necesario void Game::updateHiScore() { hi_score_.name = Options::settings.hi_score_table.front().name; // Si la puntuación actual es mayor que la máxima puntuación for (const auto &player : players_) { if (player->getScore() > hi_score_.score) { // Actualiza la máxima puntuación hi_score_.score = player->getScore(); hi_score_.name.clear(); // Si se supera la máxima puntuación emite sonido if (hi_score_achieved_ == false) { hi_score_achieved_ = true; playSound("hi_score_achieved.wav"); } } } } // Actualiza las variables del jugador void Game::updatePlayers() { for (auto &player : players_) { player->update(); if (player->isPlaying()) { // Comprueba la colisión entre el jugador y los globos auto balloon = checkPlayerBalloonCollision(player); // Si hay colisión if (balloon) { // Si el globo está parado y el temporizador activo, lo explota if (balloon->isStopped() && time_stopped_counter_ > 0) { balloon_manager_->popBalloon(balloon); } // En caso contrario, el jugador ha sido golpeado por un globo activo else { handlePlayerCollision(player); if (demo_.enabled && allPlayersAreNotPlaying()) { fade_out_->setType(FadeType::RANDOM_SQUARE); fade_out_->activate(); } } } // Comprueba las colisiones entre el jugador y los items checkPlayerItemCollision(player); } } // Organiza la lista de jugadores movePlayersToFront(); } // Dibuja a los jugadores void Game::renderPlayers() { for (auto &player : players_) { if (!player->isWaiting()) { player->render(); } } } // Comprueba si hay cambio de fase y actualiza las variables void Game::updateStage() { if (Stage::power >= Stage::get(Stage::number).power_to_complete) { // Cambio de fase Stage::power = Stage::get(Stage::number).power_to_complete - Stage::power; ++Stage::number; playSound("stage_change.wav"); balloon_manager_->resetBalloonSpeed(); screen_->flash(FLASH_COLOR, 3); screen_->shake(); // Escribe el texto por pantalla if (Stage::number < 10) { std::vector paths = {paths_.at(2), paths_.at(3)}; if (Stage::number == 9) { createMessage(paths, Resource::get()->getTexture("game_text_last_stage")); } else { auto text = Resource::get()->getText("04b_25_2x"); const std::string CAPTION = std::to_string(10 - Stage::number) + Lang::getText("[GAME_TEXT] 2"); createMessage(paths, text->writeToTexture(CAPTION, 1, -4)); } } // Modifica el color de fondo al llegar a la Fase 10 if (Stage::number == 9) { background_->setColor(Color(0xdd, 0x19, 0x1d).DARKEN()); background_->setAlpha(96); } } } // Actualiza el estado de fin de la partida void Game::updateGameStateGameOver() { fade_out_->update(); updatePlayers(); updateScoreboard(); updateBackground(); balloon_manager_->update(); tabe_->update(); updateBullets(); updateItems(); updateSmartSprites(); updatePathSprites(); updateTimeStopped(); checkBulletCollision(); cleanVectors(); if (game_over_counter_ > 0) { if (game_over_counter_ == GAME_OVER_COUNTER_) { createMessage({paths_.at(2), paths_.at(3)}, Resource::get()->getTexture("game_text_game_over")); Audio::get()->fadeOutMusic(1000); balloon_manager_->setBouncingSounds(true); } game_over_counter_--; if (game_over_counter_ == 150) { fade_out_->activate(); } } if (fade_out_->isEnabled()) { if (Options::audio.enabled) { const float VOL = static_cast(64 * (100 - fade_out_->getValue())) / 100.0f; Audio::get()->setSoundVolume(static_cast(VOL), Audio::Group::GAME); } } if (fade_out_->hasEnded()) { if (game_completed_counter_ > 0) { // Los jugadores han completado el juego Section::name = Section::Name::CREDITS; } else { // La partida ha terminado con la derrota de los jugadores Section::name = Section::Name::HI_SCORE_TABLE; } Section::options = Section::Options::HI_SCORE_AFTER_PLAYING; if (Options::audio.enabled) { Audio::get()->stopAllSounds(); Audio::get()->setSoundVolume(Options::audio.sound.volume, Audio::Group::GAME); } } } // Gestiona eventos para el estado del final del juego void Game::updateGameStateCompleted() { constexpr int START_CELEBRATIONS = 400; constexpr int END_CELEBRATIONS = START_CELEBRATIONS + 300; updatePlayers(); updateScoreboard(); updateBackground(); balloon_manager_->update(); tabe_->update(); updateBullets(); updateItems(); updateSmartSprites(); updatePathSprites(); cleanVectors(); // Para la música y elimina todos los globos e items if (game_completed_counter_ == 0) { stopMusic(); Stage::number = 9; // Deja el valor dentro de los limites balloon_manager_->destroyAllBalloons(); // Destruye a todos los globos playSound("power_ball_explosion.wav"); destroyAllItems(); // Destruye todos los items Stage::power = 0; // Vuelve a dejar el poder a cero, por lo que hubiera podido subir al destruir todos los globos background_->setAlpha(0); // Elimina el tono rojo de las últimas pantallas } // Comienza las celebraciones // Muestra el mensaje de felicitación y da los puntos a los jugadores if (game_completed_counter_ == START_CELEBRATIONS) { createMessage({paths_.at(4), paths_.at(5)}, Resource::get()->getTexture("game_text_congratulations")); createMessage({paths_.at(6), paths_.at(7)}, Resource::get()->getTexture("game_text_1000000_points")); for (auto &player : players_) if (player->isPlaying()) { player->addScore(1000000); player->setPlayingState(PlayerState::CELEBRATING); } else { player->setPlayingState(PlayerState::GAME_OVER); } updateHiScore(); } // Termina las celebraciones if (game_completed_counter_ == END_CELEBRATIONS) { for (auto &player : players_) { if (player->isCelebrating()) { player->setPlayingState(player->IsEligibleForHighScore() ? PlayerState::ENTERING_NAME_GAME_COMPLETED : PlayerState::LEAVING_SCREEN); } } } // Si los jugadores ya no estan y no quedan mensajes en pantalla if (allPlayersAreGameOver() && path_sprites_.size() == 0) { setState(GameState::GAME_OVER); } // Incrementa el contador al final ++game_completed_counter_; } // Comprueba el estado del juego void Game::checkState() { if (state_ != GameState::COMPLETED && Stage::number == 10) { setState(GameState::COMPLETED); } if (state_ != GameState::GAME_OVER && allPlayersAreGameOver()) { setState(GameState::GAME_OVER); } } // Destruye todos los items void Game::destroyAllItems() { for (auto &item : items_) item->disable(); } // Comprueba la colisión entre el jugador y los globos activos std::shared_ptr Game::checkPlayerBalloonCollision(std::shared_ptr &player) { for (auto &balloon : balloon_manager_->getBalloons()) { if (!balloon->isInvulnerable() && !balloon->isPowerBall()) { if (checkCollision(player->getCollider(), balloon->getCollider())) { return balloon; // Devuelve el globo con el que se ha producido la colisión } } } return nullptr; // No se ha producido ninguna colisión } // Comprueba la colisión entre el jugador y los items void Game::checkPlayerItemCollision(std::shared_ptr &player) { if (!player->isPlaying()) return; for (auto &item : items_) { if (item->isEnabled()) { if (checkCollision(player->getCollider(), item->getCollider())) { switch (item->getType()) { case ItemType::DISK: { player->addScore(1000); const auto X = item->getPosX() + (item->getWidth() - game_text_textures_.at(0)->getWidth()) / 2; createItemText(X, game_text_textures_.at(0)); playSound("item_pickup.wav"); break; } case ItemType::GAVINA: { player->addScore(2500); const auto X = item->getPosX() + (item->getWidth() - game_text_textures_.at(1)->getWidth()) / 2; createItemText(X, game_text_textures_.at(1)); playSound("item_pickup.wav"); break; } case ItemType::PACMAR: { player->addScore(5000); const auto X = item->getPosX() + (item->getWidth() - game_text_textures_.at(2)->getWidth()) / 2; createItemText(X, game_text_textures_.at(2)); playSound("item_pickup.wav"); break; } case ItemType::DEBIAN: { player->addScore(100000); const auto X = item->getPosX() + (item->getWidth() - game_text_textures_.at(6)->getWidth()) / 2; createItemText(X, game_text_textures_.at(6)); playSound("debian_pickup.wav"); break; } case ItemType::CLOCK: { enableTimeStopItem(); const auto X = item->getPosX() + (item->getWidth() - game_text_textures_.at(5)->getWidth()) / 2; createItemText(X, game_text_textures_.at(5)); playSound("item_pickup.wav"); break; } case ItemType::COFFEE: { if (player->getCoffees() == 2) { player->addScore(5000); const auto X = item->getPosX() + (item->getWidth() - game_text_textures_.at(2)->getWidth()) / 2; createItemText(X, game_text_textures_.at(2)); } else { player->giveExtraHit(); const auto X = item->getPosX() + (item->getWidth() - game_text_textures_.at(4)->getWidth()) / 2; createItemText(X, game_text_textures_.at(4)); } playSound("voice_coffee.wav"); break; } case ItemType::COFFEE_MACHINE: { player->setPowerUp(); coffee_machine_enabled_ = false; const auto X = item->getPosX() + (item->getWidth() - game_text_textures_.at(3)->getWidth()) / 2; createItemText(X, game_text_textures_.at(3)); playSound("voice_power_up.wav"); break; } default: break; } updateHiScore(); item->disable(); } } } } // Comprueba y procesa la colisión de las balas void Game::checkBulletCollision() { for (auto &bullet : bullets_) { // Comprueba la colisión con el Tabe if (bullet->isEnabled() && tabe_->isEnabled()) if (checkCollision(bullet->getCollider(), tabe_->getCollider())) { tabe_->setState(TabeState::HIT); bullet->disable(); auto pos = tabe_->getCollider(); if (tabe_->tryToGetBonus()) { createItem(ItemType::DEBIAN, pos.x, pos.y); playSound("debian_drop.wav"); } else { if (rand() % 3 == 0) { createItem(ItemType::COFFEE, pos.x, pos.y); } playSound("tabe_hit.wav"); } break; } // Comprueba la colisión con los globos for (auto &balloon : balloon_manager_->getBalloons()) { if (balloon->isEnabled() && (!balloon->isInvulnerable()) && bullet->isEnabled()) { if (checkCollision(balloon->getCollider(), bullet->getCollider())) { // Obtiene al jugador que disparó la bala auto player = getPlayer(bullet->getOwner()); // Suelta el item si se da el caso const auto DROPPED_ITEM = dropItem(); if (DROPPED_ITEM != ItemType::NONE && !demo_.recording) { if (DROPPED_ITEM != ItemType::COFFEE_MACHINE) { createItem(DROPPED_ITEM, balloon->getPosX(), balloon->getPosY()); } else { createItem(DROPPED_ITEM, player->getPosX(), param.game.game_area.rect.y - Item::COFFEE_MACHINE_HEIGHT); coffee_machine_enabled_ = true; } } // Explota el globo const auto SCORE = balloon_manager_->popBalloon(balloon); evaluateAndSetMenace(); // Otorga los puntos al jugador que disparó la bala if (player->isPlaying()) { player->addScore(SCORE * player->getScoreMultiplier() * difficulty_score_multiplier_); player->incScoreMultiplier(); } updateHiScore(); // Deshabilita la bala bullet->disable(); break; } } } } } // Mueve las balas activas void Game::updateBullets() { for (auto &bullet : bullets_) { if (bullet->update() == BulletMoveStatus::OUT) { getPlayer(bullet->getOwner())->decScoreMultiplier(); } } } // Pinta las balas activas void Game::renderBullets() { for (auto &bullet : bullets_) bullet->render(); } // Crea un objeto bala void Game::createBullet(int x, int y, BulletType kind, bool powered_up, int owner) { bullets_.emplace_back(std::make_unique(x, y, kind, powered_up, owner)); } // Vacia el vector de balas void Game::freeBullets() { if (!bullets_.empty()) for (int i = bullets_.size() - 1; i >= 0; --i) if (!bullets_[i]->isEnabled()) bullets_.erase(bullets_.begin() + i); } // Actualiza los items void Game::updateItems() { for (auto &item : items_) if (item->isEnabled()) { item->update(); if (item->isOnFloor()) { playSound("title.wav"); screen_->shake(1, 2, 4); } } } // Pinta los items activos void Game::renderItems() { for (auto &item : items_) item->render(); } // Devuelve un item al azar y luego segun sus probabilidades ItemType Game::dropItem() { const auto LUCKY_NUMBER = rand() % 100; const auto ITEM = rand() % 6; switch (ITEM) { case 0: if (LUCKY_NUMBER < helper_.item_disk_odds) { return ItemType::DISK; } break; case 1: if (LUCKY_NUMBER < helper_.item_gavina_odds) { return ItemType::GAVINA; } break; case 2: if (LUCKY_NUMBER < helper_.item_pacmar_odds) { return ItemType::GAVINA; } break; case 3: if (LUCKY_NUMBER < helper_.item_clock_odds) { return ItemType::CLOCK; } break; case 4: if (LUCKY_NUMBER < helper_.item_coffee_odds) { helper_.item_coffee_odds = ITEM_COFFEE_ODDS_; return ItemType::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 ItemType::COFFEE_MACHINE; } } else { if (helper_.need_coffee_machine) { helper_.item_coffee_machine_odds++; } } break; default: break; } return ItemType::NONE; } // Crea un objeto item void Game::createItem(ItemType type, float x, float y) { items_.emplace_back(std::make_unique(type, x, y, param.game.play_area.rect, item_textures_[static_cast(type) - 1], item_animations_[static_cast(type) - 1])); playSound("item_drop.wav"); } // Vacia el vector de items void Game::freeItems() { if (!items_.empty()) for (int i = items_.size() - 1; i >= 0; --i) if (!items_[i]->isEnabled()) items_.erase(items_.begin() + i); } // Crea un objeto PathSprite void Game::createItemText(int x, std::shared_ptr texture) { path_sprites_.emplace_back(std::make_unique(texture)); const auto W = texture->getWidth(); const auto H = texture->getHeight(); const int Y0 = param.game.play_area.rect.h - H; const int Y1 = 160 - (H / 2); const int Y2 = -H; // Ajusta para que no se dibuje fuera de pantalla x = std::clamp(x, 2, static_cast(param.game.play_area.rect.w) - W - 2); // Inicializa path_sprites_.back()->setWidth(W); path_sprites_.back()->setHeight(H); path_sprites_.back()->setSpriteClip({0, 0, static_cast(W), static_cast(H)}); path_sprites_.back()->addPath(Y0, Y1, PathType::VERTICAL, x, 100, easeOutQuint, 0); path_sprites_.back()->addPath(Y1, Y2, PathType::VERTICAL, x, 80, easeInQuint, 0); path_sprites_.back()->enable(); } // Crea un objeto PathSprite void Game::createMessage(const std::vector &paths, std::shared_ptr texture) { path_sprites_.emplace_back(std::make_unique(texture)); // Inicializa for (const auto &path : paths) { path_sprites_.back()->addPath(path, true); } path_sprites_.back()->enable(); } // 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()) smart_sprites_.erase(smart_sprites_.begin() + i); } // Vacia el vector de pathsprites void Game::freePathSprites() { if (!path_sprites_.empty()) for (int i = path_sprites_.size() - 1; i >= 0; --i) if (path_sprites_[i]->hasFinished()) path_sprites_.erase(path_sprites_.begin() + i); } // Crea un SpriteSmart para arrojar el item café al recibir un impacto void Game::throwCoffee(int x, int y) { smart_sprites_.emplace_back(std::make_unique(item_textures_[4])); smart_sprites_.back()->setPosX(x - 8); smart_sprites_.back()->setPosY(y - 8); smart_sprites_.back()->setWidth(param.game.item_size); smart_sprites_.back()->setHeight(param.game.item_size); smart_sprites_.back()->setVelX(-1.0f + ((rand() % 5) * 0.5f)); smart_sprites_.back()->setVelY(-4.0f); smart_sprites_.back()->setAccelX(0.0f); smart_sprites_.back()->setAccelY(0.2f); smart_sprites_.back()->setDestX(x + (smart_sprites_.back()->getVelX() * 50)); smart_sprites_.back()->setDestY(param.game.height + 1); smart_sprites_.back()->setEnabled(true); smart_sprites_.back()->setFinishedCounter(1); smart_sprites_.back()->setSpriteClip(0, param.game.item_size, param.game.item_size, param.game.item_size); smart_sprites_.back()->setRotatingCenter({param.game.item_size / 2, param.game.item_size / 2}); smart_sprites_.back()->setRotate(true); smart_sprites_.back()->setRotateSpeed(10); smart_sprites_.back()->setRotateAmount(90.0); } // Actualiza los SmartSprites void Game::updateSmartSprites() { for (auto &sprite : smart_sprites_) { sprite->update(); } } // Pinta los SmartSprites activos void Game::renderSmartSprites() { for (auto &sprite : smart_sprites_) { sprite->render(); } } // Actualiza los PathSprites void Game::updatePathSprites() { for (auto &sprite : path_sprites_) { sprite->update(); } } // Pinta los PathSprites activos void Game::renderPathSprites() { for (auto &sprite : path_sprites_) { sprite->render(); } } // Acciones a realizar cuando el jugador colisiona con un globo void Game::handlePlayerCollision(std::shared_ptr &player) { if (!player->isPlaying() || player->isInvulnerable()) { // Si no está jugando o tiene inmunidad, no hace nada return; } // Si tiene cafes if (player->hasExtraHit()) { // Lo pierde player->removeExtraHit(); throwCoffee(player->getPosX() + (player->getWidth() / 2), player->getPosY() + (player->getHeight() / 2)); playSound("coffee_out.wav"); screen_->shake(); } else { // Si no tiene cafes, muere playSound("player_collision.wav"); if (param.game.hit_stop) { SDL_Delay(param.game.hit_stop_ms); } screen_->shake(); playSound("voice_no.wav"); player->setPlayingState(PlayerState::ROLLING); players_to_reorder.push_back(player); if (allPlayersAreNotPlaying()) { // No se puede subir poder de fase si no hay nadie jugando Stage::power_can_be_added = false; } } } // Actualiza y comprueba el valor de la variable void Game::updateTimeStopped() { if (time_stopped_counter_ > 0) { time_stopped_counter_--; if (time_stopped_counter_ > 120) { if (time_stopped_counter_ % 30 == 0) { playSound("clock.wav"); } } else { if (time_stopped_counter_ % 30 == 0) { balloon_manager_->normalColorsToAllBalloons(); playSound("clock.wav"); } else if (time_stopped_counter_ % 30 == 15) { balloon_manager_->reverseColorsToAllBalloons(); playSound("clock.wav"); } } } else { disableTimeStopItem(); } } // Actualiza el juego void Game::update() { if (SDL_GetTicks() - ticks_ > param.game.speed) { ticks_ = SDL_GetTicks(); checkServiceMenu(); updateDemo(); #ifdef RECORDING updateRecording(); #endif if (!paused_) { switch (state_) { case GameState::FADE_IN: updateGameStateFadeIn(); break; case GameState::ENTERING_PLAYER: updateGameStateEnteringPlayer(); break; case GameState::SHOWING_GET_READY_MESSAGE: updateGameStateShowingGetReadyMessage(); break; case GameState::PLAYING: updateGameStatePlaying(); break; case GameState::COMPLETED: updateGameStateCompleted(); break; case GameState::GAME_OVER: updateGameStateGameOver(); break; default: break; } } screen_->update(); fillCanvas(); } } // Actualiza el fondo void Game::updateBackground() { // Si el juego está completado, se reduce la velocidad de las nubes if (state_ == GameState::COMPLETED) { Stage::total_power = (Stage::total_power > 200) ? (Stage::total_power - 25) : 200; } // Calcula la velocidad en función de los globos explotados y el total de globos a explotar para acabar el juego constexpr float CLOUDS_INITIAL_SPEED = 0.05f; constexpr float CLOUDS_FINAL_SPEED = 2.00f - CLOUDS_INITIAL_SPEED; const float CLOUDS_SPEED = (-CLOUDS_INITIAL_SPEED) + (-CLOUDS_FINAL_SPEED * (static_cast(Stage::total_power) / total_power_to_complete_game_)); background_->setCloudsSpeed(CLOUDS_SPEED); // Calcula la transición de los diferentes fondos constexpr float NUM = 1525.0f; // total_power_to_complete div 4 const float GRADIENT_NUMBER = std::min(Stage::total_power / NUM, 3.0f); const float PERCENT = GRADIENT_NUMBER - static_cast(GRADIENT_NUMBER); background_->setGradientNumber(static_cast(GRADIENT_NUMBER)); background_->setTransition(PERCENT); // Calcula la posición del sol constexpr float SUN_FINAL_POWER = NUM * 2; background_->setSunProgression(Stage::total_power / SUN_FINAL_POWER); background_->setMoonProgression(Stage::total_power / static_cast(total_power_to_complete_game_)); // Actualiza el objeto background_->update(); } // Dibuja los elementos de la zona de juego en su textura void Game::fillCanvas() { // Dibujamos el contenido de la zona de juego en su textura auto temp = SDL_GetRenderTarget(renderer_); SDL_SetRenderTarget(renderer_, canvas_); // Dibuja los objetos background_->render(); renderPlayers(); renderSmartSprites(); renderItems(); balloon_manager_->render(); tabe_->render(); renderBullets(); renderPathSprites(); // Deja el renderizador apuntando donde estaba SDL_SetRenderTarget(renderer_, temp); } // Dibuja el juego void Game::render() { // Prepara para empezar a dibujar en la textura de juego screen_->start(); // Copia la textura con la zona de juego a la pantalla SDL_RenderTexture(renderer_, canvas_, nullptr, ¶m.game.play_area.rect); // Dibuja el marcador scoreboard_->render(); // Dibuja el fade fade_in_->render(); fade_out_->render(); // Vuelca el contenido del renderizador en pantalla screen_->render(); } // Habilita el efecto del item de detener el tiempo void Game::enableTimeStopItem() { balloon_manager_->stopAllBalloons(); balloon_manager_->reverseColorsToAllBalloons(); time_stopped_counter_ = TIME_STOPPED_COUNTER_; } // Deshabilita el efecto del item de detener el tiempo void Game::disableTimeStopItem() { time_stopped_counter_ = 0; balloon_manager_->startAllBalloons(); balloon_manager_->normalColorsToAllBalloons(); } // Bucle para el juego void Game::run() { while (Section::name == Section::Name::GAME) { #ifndef RECORDING checkInput(); #endif update(); checkEvents(); // Tiene que ir antes del render render(); } } // Inicializa las variables que contienen puntos de ruta para mover objetos void Game::initPaths() { // Recorrido para el texto de "Get Ready!" (0,1) { const auto &texture = Resource::get()->getTexture("game_text_get_ready"); const auto W = texture->getWidth(); const int X0 = -W; const int X1 = param.game.play_area.center_x - W / 2; const int X2 = param.game.play_area.rect.w; const int Y = param.game.play_area.center_y; paths_.emplace_back(Path(createPath(X0, X1, PathType::HORIZONTAL, Y, 80, easeOutQuint), 20)); paths_.emplace_back(Path(createPath(X1, X2, PathType::HORIZONTAL, Y, 80, easeInQuint), 0)); } // Recorrido para el texto de "Last Stage!" o de "X stages left" o "Game Over" (2,3) { const auto &texture = Resource::get()->getTexture("game_text_last_stage"); const auto H = texture->getHeight(); const int Y0 = param.game.play_area.rect.h - H; const int Y1 = param.game.play_area.center_y - H / 2; const int Y2 = -H; const int X = param.game.play_area.center_x; paths_.emplace_back(Path(createPath(Y0, Y1, PathType::VERTICAL, X, 80, easeOutQuint), 20)); paths_.emplace_back(Path(createPath(Y1, Y2, PathType::VERTICAL, X, 80, easeInQuint), 0)); } // Recorrido para el texto de "Congratulations!!" (3,4) { const auto &texture = Resource::get()->getTexture("game_text_congratulations"); const auto W = texture->getWidth(); const auto H = texture->getHeight(); const int X0 = -W; const int X1 = param.game.play_area.center_x - W / 2; const int X2 = param.game.play_area.rect.w; const int Y = param.game.play_area.center_y - H / 2 - 20; paths_.emplace_back(Path(createPath(X0, X1, PathType::HORIZONTAL, Y, 80, easeOutQuint), 400)); paths_.emplace_back(Path(createPath(X1, X2, PathType::HORIZONTAL, Y, 80, easeInQuint), 0)); } // Recorrido para el texto de "1.000.000 points!" (5,6) { const auto &texture = Resource::get()->getTexture("game_text_1000000_points"); const auto W = texture->getWidth(); const auto H = texture->getHeight(); const int X0 = param.game.play_area.rect.w; const int X1 = param.game.play_area.center_x - W / 2; const int X2 = -W; const int Y = param.game.play_area.center_y + H / 2 - 20; paths_.emplace_back(Path(createPath(X0, X1, PathType::HORIZONTAL, Y, 80, easeOutQuint), 400)); paths_.emplace_back(Path(createPath(X1, X2, PathType::HORIZONTAL, Y, 80, easeInQuint), 0)); } } // Actualiza las variables de ayuda void Game::updateHelper() { // Solo ofrece ayuda cuando la amenaza es elevada if (menace_current_ > 15) { helper_.need_coffee = true; helper_.need_coffee_machine = true; for (const auto &player : players_) { if (player->isPlaying()) { helper_.need_coffee &= (player->getCoffees() == 0); helper_.need_coffee_machine &= (!player->isPowerUp()); } } } else { helper_.need_coffee = helper_.need_coffee_machine = false; } } // Comprueba si todos los jugadores han terminado de jugar bool Game::allPlayersAreWaitingOrGameOver() { auto success = true; for (const auto &player : players_) success &= player->isWaiting() || player->isGameOver(); return success; } // Comprueba si todos los jugadores han terminado de jugar bool Game::allPlayersAreGameOver() { auto success = true; for (const auto &player : players_) success &= player->isGameOver(); return success; } // Comprueba si todos los jugadores han terminado de jugar bool Game::allPlayersAreNotPlaying() { auto success = true; for (const auto &player : players_) success &= !player->isPlaying(); return success; } // Comprueba los eventos que hay en cola void Game::checkEvents() { SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_EVENT_WINDOW_FOCUS_LOST: { pause(!demo_.enabled); break; } case SDL_EVENT_WINDOW_FOCUS_GAINED: { pause(false); break; } default: break; } #ifdef DEBUG checkDebugEvents(event); #endif GlobalEvents::check(event); } } // Actualiza el marcador void Game::updateScoreboard() { for (const auto &player : players_) { scoreboard_->setScore(player->getScoreBoardPanel(), player->getScore()); scoreboard_->setMult(player->getScoreBoardPanel(), player->getScoreMultiplier()); } // Resto de marcador scoreboard_->setStage(Stage::number + 1); scoreboard_->setPower((float)Stage::power / (float)Stage::get(Stage::number).power_to_complete); scoreboard_->setHiScore(hi_score_.score); scoreboard_->setHiScoreName(hi_score_.name); // Lógica del marcador scoreboard_->update(); } // Pausa el juego void Game::pause(bool value) { paused_ = value; screen_->attenuate(paused_); tabe_->pauseTimer(paused_); } // Añade una puntuación a la tabla de records void Game::addScoreToScoreBoard(const std::shared_ptr &player) { const auto ENTRY = HiScoreEntry(trim(player->getLastEnterName()), player->getScore(), player->get1CC()); auto manager = std::make_unique(Options::settings.hi_score_table); Options::settings.last_hi_score_entry.at(player->getId() - 1) = manager->add(ENTRY); manager->saveToFile(Asset::get()->get("score.bin")); hi_score_.name = Options::settings.hi_score_table.front().name; } // Saca del estado de GAME OVER al jugador si el otro está activo void Game::checkAndUpdatePlayerStatus(int active_player_index, int inactive_player_index) { if (players_[active_player_index]->isGameOver() && !players_[inactive_player_index]->isGameOver() && !players_[inactive_player_index]->isWaiting()) { players_[active_player_index]->setPlayingState(PlayerState::WAITING); } } // Comprueba el estado de los jugadores void Game::checkPlayersStatusPlaying() { if (demo_.enabled) { return; } // Comprueba si todos los jugadores estan esperando if (allPlayersAreWaitingOrGameOver()) { // Entonces los pone en estado de Game Over for (auto &player : players_) { player->setPlayingState(PlayerState::GAME_OVER); } } // Comprobar estado de ambos jugadores checkAndUpdatePlayerStatus(0, 1); checkAndUpdatePlayerStatus(1, 0); } // Obtiene un jugador a partir de su "id" std::shared_ptr Game::getPlayer(int id) { auto it = std::find_if(players_.begin(), players_.end(), [id](const auto &player) { return player->getId() == id; }); if (it != players_.end()) { return *it; } return nullptr; } // Obtiene un controlador a partir del "id" del jugador int Game::getController(int player_id) { auto it = std::find_if(Options::controllers.begin(), Options::controllers.end(), [player_id](const auto &controller) { return controller.player_id == player_id; }); if (it != Options::controllers.end()) { return std::distance(Options::controllers.begin(), it); } return -1; } // Gestiona la entrada durante el juego void Game::checkInput() { Input::get()->update(); // Comprueba las entradas si no está el menú de servicio activo if (!ServiceMenu::get()->isEnabled()) { checkPauseInput(); demo_.enabled ? DEMO_handlePassInput() : handlePlayersInput(); } // Mueve los jugadores en el modo demo if (demo_.enabled) { DEMO_handleInput(); } // Verifica los inputs globales. GlobalInputs::check(); } // Verifica si alguno de los controladores ha solicitado una pausa y actualiza el estado de pausa del juego. void Game::checkPauseInput() { // Comprueba los mandos for (int i = 0; i < input_->getNumControllers(); ++i) { if (input_->checkInput(InputAction::PAUSE, INPUT_DO_NOT_ALLOW_REPEAT, InputDevice::CONTROLLER, i)) { pause(!paused_); return; } } // Comprueba el teclado if (input_->checkInput(InputAction::PAUSE, INPUT_DO_NOT_ALLOW_REPEAT, InputDevice::KEYBOARD)) { pause(!paused_); return; } } // Gestiona las entradas de los jugadores en el modo demo para saltarse la demo. void Game::DEMO_handlePassInput() { if (input_->checkAnyButton()) { Section::name = Section::Name::TITLE; // Salir del modo demo y regresar al menú principal. Section::attract_mode = Section::AttractMode::TITLE_TO_DEMO; // El juego volverá a mostrar la demo return; } } // Gestiona las entradas de los jugadores en el modo demo, incluyendo movimientos y disparos automáticos. void Game::DEMO_handleInput() { int index = 0; for (const auto &player : players_) { if (player->isPlaying()) { // Maneja el input específico del jugador en modo demo. DEMO_handlePlayerInput(player, index); } ++index; } } // Procesa las entradas para un jugador específico durante el modo demo. void Game::DEMO_handlePlayerInput(const std::shared_ptr &player, int index) { const auto &demo_data = demo_.data[index][demo_.counter]; if (demo_data.left == 1) { player->setInput(InputAction::LEFT); } else if (demo_data.right == 1) { player->setInput(InputAction::RIGHT); } else if (demo_data.no_input == 1) { player->setInput(InputAction::NONE); } if (demo_data.fire == 1) { handleFireInput(player, BulletType::UP); } else if (demo_data.fire_left == 1) { handleFireInput(player, BulletType::LEFT); } else if (demo_data.fire_right == 1) { handleFireInput(player, BulletType::RIGHT); } } // Maneja el disparo de un jugador, incluyendo la creación de balas y la gestión del tiempo de espera entre disparos. void Game::handleFireInput(const std::shared_ptr &player, BulletType bullet_type) { if (player->canFire()) { SDL_Point bullet = {0, 0}; switch (bullet_type) { case BulletType::UP: player->setInput(InputAction::FIRE_CENTER); bullet.x = 2 + player->getPosX() + (player->getWidth() - Bullet::WIDTH) / 2; bullet.y = player->getPosY() - (Bullet::HEIGHT / 2); break; case BulletType::LEFT: player->setInput(InputAction::FIRE_LEFT); bullet.x = player->getPosX() - (Bullet::WIDTH / 2); bullet.y = player->getPosY(); break; case BulletType::RIGHT: player->setInput(InputAction::FIRE_RIGHT); bullet.x = player->getPosX() + player->getWidth() - (Bullet::WIDTH / 2); bullet.y = player->getPosY(); break; default: break; } createBullet(bullet.x, bullet.y, bullet_type, player->isPowerUp(), player->getId()); playSound("bullet.wav"); // Establece un tiempo de espera para el próximo disparo. constexpr int POWERUP_COOLDOWN = 5; constexpr int AUTOFIRE_COOLDOWN = 10; constexpr int NORMAL_COOLDOWN = 7; int cant_fire_counter; if (player->isPowerUp()) { cant_fire_counter = POWERUP_COOLDOWN; } else if (Options::settings.autofire) { cant_fire_counter = AUTOFIRE_COOLDOWN; } else { cant_fire_counter = NORMAL_COOLDOWN; } player->setCantFireCounter(cant_fire_counter); } } // Gestiona las entradas de todos los jugadores en el modo normal (fuera del modo demo). void Game::handlePlayersInput() { for (const auto &player : players_) { if (player->isPlaying()) { // Maneja el input de los jugadores en modo normal. handleNormalPlayerInput(player); } else if (player->isContinue() || player->isWaiting()) { // Gestiona la continuación del jugador. handlePlayerContinue(player); } else if (player->isEnteringName() || player->isEnteringNameGameCompleted() || player->isShowingName()) { // Gestiona la introducción del nombre del jugador. handleNameInput(player); } } } // Maneja las entradas de movimiento y disparo para un jugador en modo normal. void Game::handleNormalPlayerInput(const std::shared_ptr &player) { const auto &controller = Options::controllers.at(player->getController()); const bool AUTOFIRE = player->isPowerUp() || Options::settings.autofire; if (input_->checkInput(InputAction::LEFT, INPUT_ALLOW_REPEAT, controller.type, controller.index)) { player->setInput(InputAction::LEFT); #ifdef RECORDING demo_.keys.left = 1; #endif } else if (input_->checkInput(InputAction::RIGHT, INPUT_ALLOW_REPEAT, controller.type, controller.index)) { player->setInput(InputAction::RIGHT); #ifdef RECORDING demo_.keys.right = 1; #endif } else { player->setInput(InputAction::NONE); #ifdef RECORDING demo_.keys.no_input = 1; #endif } handleFireInputs(player, AUTOFIRE, player->getController()); // Verifica y maneja todas las posibles entradas de disparo. } // Procesa las entradas de disparo del jugador, permitiendo disparos automáticos si está habilitado. void Game::handleFireInputs(const std::shared_ptr &player, bool autofire, int controller_index) { if (input_->checkInput(InputAction::FIRE_CENTER, autofire, Options::controllers[controller_index].type, Options::controllers[controller_index].index)) { handleFireInput(player, BulletType::UP); #ifdef RECORDING demo_.keys.fire = 1; #endif } else if (input_->checkInput(InputAction::FIRE_LEFT, autofire, Options::controllers[controller_index].type, Options::controllers[controller_index].index)) { handleFireInput(player, BulletType::LEFT); #ifdef RECORDING demo_.keys.fire_left = 1; #endif } else if (input_->checkInput(InputAction::FIRE_RIGHT, autofire, Options::controllers[controller_index].type, Options::controllers[controller_index].index)) { handleFireInput(player, BulletType::RIGHT); #ifdef RECORDING demo_.keys.fire_right = 1; #endif } } // Maneja la continuación del jugador cuando no está jugando, permitiendo que continúe si se pulsa el botón de inicio. void Game::handlePlayerContinue(const std::shared_ptr &player) { const auto CONTROLLER_INDEX = player->getController(); if (input_->checkInput(InputAction::START, INPUT_DO_NOT_ALLOW_REPEAT, Options::controllers[CONTROLLER_INDEX].type, Options::controllers[CONTROLLER_INDEX].index)) { player->setPlayingState(PlayerState::RESPAWNING); } // Disminuye el contador de continuación si se presiona cualquier botón de disparo. if (input_->checkInput(InputAction::FIRE_LEFT, INPUT_DO_NOT_ALLOW_REPEAT, Options::controllers[CONTROLLER_INDEX].type, Options::controllers[CONTROLLER_INDEX].index) || input_->checkInput(InputAction::FIRE_CENTER, INPUT_DO_NOT_ALLOW_REPEAT, Options::controllers[CONTROLLER_INDEX].type, Options::controllers[CONTROLLER_INDEX].index) || input_->checkInput(InputAction::FIRE_RIGHT, INPUT_DO_NOT_ALLOW_REPEAT, Options::controllers[CONTROLLER_INDEX].type, Options::controllers[CONTROLLER_INDEX].index)) { if (player->getContinueCounter() < param.scoreboard.skip_countdown_value) { player->decContinueCounter(); } } } // Procesa las entradas para la introducción del nombre del jugador. void Game::handleNameInput(const std::shared_ptr &player) { const auto CONTROLLER_INDEX = player->getController(); if (input_->checkInput(InputAction::FIRE_LEFT, INPUT_DO_NOT_ALLOW_REPEAT, Options::controllers[CONTROLLER_INDEX].type, Options::controllers[CONTROLLER_INDEX].index)) { if (player->isShowingName()) { player->setPlayingState(PlayerState::CONTINUE); } else if (player->getEnterNamePositionOverflow()) { player->setInput(InputAction::START); addScoreToScoreBoard(player); player->setPlayingState(PlayerState::SHOWING_NAME); } else { player->setInput(InputAction::RIGHT); } } else if (input_->checkInput(InputAction::FIRE_CENTER, INPUT_DO_NOT_ALLOW_REPEAT, Options::controllers[CONTROLLER_INDEX].type, Options::controllers[CONTROLLER_INDEX].index) || input_->checkInput(InputAction::FIRE_RIGHT, INPUT_DO_NOT_ALLOW_REPEAT, Options::controllers[CONTROLLER_INDEX].type, Options::controllers[CONTROLLER_INDEX].index)) { if (player->isShowingName()) { player->setPlayingState(PlayerState::CONTINUE); } else { player->setInput(InputAction::LEFT); } } else if (input_->checkInput(InputAction::UP, INPUT_DO_NOT_ALLOW_REPEAT, Options::controllers[CONTROLLER_INDEX].type, Options::controllers[CONTROLLER_INDEX].index)) { player->setInput(InputAction::UP); } else if (input_->checkInput(InputAction::DOWN, INPUT_DO_NOT_ALLOW_REPEAT, Options::controllers[CONTROLLER_INDEX].type, Options::controllers[CONTROLLER_INDEX].index)) { player->setInput(InputAction::DOWN); } else if (input_->checkInput(InputAction::START, INPUT_DO_NOT_ALLOW_REPEAT, Options::controllers[CONTROLLER_INDEX].type, Options::controllers[CONTROLLER_INDEX].index)) { if (player->isShowingName()) { player->setPlayingState(PlayerState::CONTINUE); } else { player->setInput(InputAction::START); addScoreToScoreBoard(player); player->setPlayingState(PlayerState::SHOWING_NAME); } } } // Inicializa las variables para el modo DEMO void Game::initDemo(int player_id) { if (demo_.enabled) { // Cambia el estado del juego setState(GameState::PLAYING); // Aleatoriza la asignación del fichero con los datos del modo demostracion { const auto DEMO1 = rand() % 2; const auto DEMO2 = (DEMO1 == 0) ? 1 : 0; demo_.data.emplace_back(Resource::get()->getDemoData(DEMO1)); demo_.data.emplace_back(Resource::get()->getDemoData(DEMO2)); } // Selecciona una pantalla al azar { constexpr auto NUM_DEMOS = 3; const auto DEMO = rand() % NUM_DEMOS; const int STAGES[NUM_DEMOS] = {0, 3, 5}; Stage::number = STAGES[DEMO]; } // Actualiza el numero de globos explotados según la fase del modo demostración for (int i = 0; i < Stage::number; ++i) { Stage::total_power += Stage::get(i).power_to_complete; } // Activa o no al otro jugador if (rand() % 3 != 0) { const auto OTHER_PLAYER_ID = player_id == 1 ? 2 : 1; auto other_player = getPlayer(OTHER_PLAYER_ID); other_player->setPlayingState(PlayerState::PLAYING); } // Asigna cafes a los jugadores for (auto &player : players_) { for (int i = 0; i < rand() % 3; ++i) player->giveExtraHit(); player->setInvulnerable(true); } // Configura los marcadores scoreboard_->setMode(SCOREBOARD_LEFT_PANEL, ScoreboardMode::DEMO); scoreboard_->setMode(SCOREBOARD_RIGHT_PANEL, ScoreboardMode::DEMO); // Silencia los globos balloon_manager_->setSounds(false); } // Modo grabar demo #ifdef RECORDING demo_.recording = true; #else demo_.recording = false; #endif demo_.counter = 0; } // Calcula el poder total necesario para completar el juego void Game::setTotalPower() { total_power_to_complete_game_ = 0; for (const auto &stage : Stage::stages) { total_power_to_complete_game_ += stage.power_to_complete; } } // Inicializa el marcador void Game::initScoreboard() { scoreboard_->setPos(param.scoreboard.rect); scoreboard_->setMode(SCOREBOARD_CENTER_PANEL, ScoreboardMode::STAGE_INFO); for (const auto &player : players_) { scoreboard_->setName(player->getScoreBoardPanel(), player->getName()); if (player->isWaiting()) { scoreboard_->setMode(player->getScoreBoardPanel(), ScoreboardMode::WAITING); } } } // Inicializa las opciones relacionadas con la dificultad void Game::initDifficultyVars() { // Variables relacionadas con la dificultad switch (difficulty_) { case Options::DifficultyCode::EASY: { balloon_manager_->setDefaultBalloonSpeed(BALLOON_SPEED[0]); difficulty_score_multiplier_ = 0.5f; scoreboard_->setColor(param.scoreboard.easy_color); break; } case Options::DifficultyCode::NORMAL: { balloon_manager_->setDefaultBalloonSpeed(BALLOON_SPEED[0]); difficulty_score_multiplier_ = 1.0f; scoreboard_->setColor(param.scoreboard.normal_color); break; } case Options::DifficultyCode::HARD: { balloon_manager_->setDefaultBalloonSpeed(BALLOON_SPEED[4]); difficulty_score_multiplier_ = 1.5f; scoreboard_->setColor(param.scoreboard.hard_color); break; } default: break; } balloon_manager_->resetBalloonSpeed(); } // Inicializa los jugadores void Game::initPlayers(int player_id) { // Crea los dos jugadores constexpr int PLAYER_HEIGHT = 32; constexpr int PLAYER_WIDTH = 32; const int Y = param.game.play_area.rect.h - PLAYER_HEIGHT + 1; players_.emplace_back(std::make_unique(1, param.game.play_area.first_quarter_x - (PLAYER_WIDTH / 2), Y, demo_.enabled, param.game.play_area.rect, player_textures_[0], player_animations_)); players_.back()->setScoreBoardPanel(SCOREBOARD_LEFT_PANEL); players_.back()->setName(Lang::getText("[SCOREBOARD] 1")); players_.back()->setController(getController(players_.back()->getId())); players_.emplace_back(std::make_unique(2, param.game.play_area.third_quarter_x - (PLAYER_WIDTH / 2), Y, demo_.enabled, param.game.play_area.rect, player_textures_[1], player_animations_)); players_.back()->setScoreBoardPanel(SCOREBOARD_RIGHT_PANEL); players_.back()->setName(Lang::getText("[SCOREBOARD] 2")); players_.back()->setController(getController(players_.back()->getId())); // Activa el jugador que coincide con el "player_id" o ambos si es "0" if (player_id == 0) { // Activa ambos jugadores getPlayer(1)->setPlayingState(demo_.enabled ? PlayerState::PLAYING : PlayerState::ENTERING_SCREEN); getPlayer(2)->setPlayingState(demo_.enabled ? PlayerState::PLAYING : PlayerState::ENTERING_SCREEN); } else { getPlayer(player_id)->setPlayingState(demo_.enabled ? PlayerState::PLAYING : PlayerState::ENTERING_SCREEN); } } // Hace sonar la música void Game::playMusic() { Audio::get()->playMusic("playing.ogg"); } // Detiene la música void Game::stopMusic() { if (!demo_.enabled) { Audio::get()->stopMusic(); } } // Actualiza las variables durante el modo demo void Game::updateDemo() { if (demo_.enabled) { balloon_manager_->setCreationTimeEnabled((balloon_manager_->getNumBalloons() == 0) ? false : true); // Actualiza ambos fades fade_in_->update(); fade_out_->update(); // Incrementa el contador de la demo if (demo_.counter < TOTAL_DEMO_DATA) { demo_.counter++; } // Activa el fundido antes de acabar con los datos de la demo if (demo_.counter == TOTAL_DEMO_DATA - 200) { fade_out_->setType(FadeType::RANDOM_SQUARE); fade_out_->activate(); } // Si ha terminado el fundido, cambia de sección if (fade_out_->hasEnded()) { Section::name = Section::Name::HI_SCORE_TABLE; return; } } } #ifdef RECORDING // Actualiza las variables durante el modo de grabación void Game::updateRecording() { // Solo mira y guarda el input en cada update checkInput(); // Incrementa el contador de la demo if (demo_.counter < TOTAL_DEMO_DATA) demo_.counter++; // Si se ha llenado el vector con datos, sale del programa else { section::name = section::Name::QUIT; return; } } #endif // Actualiza las variables durante dicho estado void Game::updateGameStateFadeIn() { fade_in_->update(); updateScoreboard(); updateBackground(); if (fade_in_->hasEnded()) { setState(GameState::ENTERING_PLAYER); balloon_manager_->createTwoBigBalloons(); evaluateAndSetMenace(); } } // Actualiza las variables durante dicho estado void Game::updateGameStateEnteringPlayer() { balloon_manager_->update(); updatePlayers(); updateScoreboard(); updateBackground(); for (const auto &player : players_) { if (player->isPlaying()) { setState(GameState::SHOWING_GET_READY_MESSAGE); createMessage({paths_.at(0), paths_.at(1)}, Resource::get()->getTexture("game_text_get_ready")); playSound("voice_get_ready.wav"); } } } // Actualiza las variables durante dicho estado void Game::updateGameStateShowingGetReadyMessage() { balloon_manager_->update(); updatePathSprites(); updatePlayers(); updateBullets(); updateScoreboard(); updateBackground(); freePathSprites(); if (path_sprites_.size() == 0) { setState(GameState::PLAYING); } if (counter_ == 100) { playMusic(); } ++counter_; } // Actualiza las variables durante el transcurso normal del juego void Game::updateGameStatePlaying() { #ifdef DEBUG if (auto_pop_balloons_) { Stage::addPower(5); } #endif updatePlayers(); checkPlayersStatusPlaying(); updateScoreboard(); updateBackground(); balloon_manager_->update(); tabe_->update(); updateBullets(); updateItems(); updateStage(); updateSmartSprites(); updatePathSprites(); updateTimeStopped(); updateHelper(); checkBulletCollision(); updateMenace(); checkAndUpdateBalloonSpeed(); checkState(); cleanVectors(); // playMusic(); } // Vacía los vectores de elementos deshabilitados void Game::cleanVectors() { freeBullets(); balloon_manager_->freeBalloons(); freeItems(); freeSmartSprites(); freePathSprites(); } // Gestiona el nivel de amenaza void Game::updateMenace() { if (state_ == GameState::PLAYING) { const auto STAGE = Stage::get(Stage::number); const float PERCENT = Stage::power / STAGE.power_to_complete; const int DIFFERENCE = STAGE.max_menace - STAGE.min_menace; // Aumenta el nivel de amenaza en función de la puntuación menace_threshold_ = 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 balloon_manager_->deployBalloonFormation(Stage::number); // Recalcula el nivel de amenaza con el nuevo globo evaluateAndSetMenace(); } } } // Calcula y establece el valor de amenaza en funcion de los globos activos void Game::evaluateAndSetMenace() { menace_current_ = balloon_manager_->getMenace(); } // Actualiza la velocidad de los globos en funcion del poder acumulado de la fase void Game::checkAndUpdateBalloonSpeed() { if (difficulty_ != Options::DifficultyCode::NORMAL) return; const float PERCENT = static_cast(Stage::power) / Stage::get(Stage::number).power_to_complete; const float THRESHOLDS[] = {0.2f, 0.4f, 0.6f, 0.8f}; for (size_t i = 0; i < std::size(THRESHOLDS); ++i) { if (balloon_manager_->getBalloonSpeed() == BALLOON_SPEED[i] && PERCENT > THRESHOLDS[i]) { balloon_manager_->setBalloonSpeed(BALLOON_SPEED[i + 1]); break; // Salir del bucle una vez actualizada la velocidad y aplicada } } } // Cambia el estado del juego void Game::setState(GameState state) { state_ = state; counter_ = 0; } void Game::playSound(const std::string &name) { if (demo_.enabled) return; static auto audio_ = Audio::get(); audio_->playSound(name); } // Organiza los jugadores para que los vivos se pinten sobre los muertos void Game::movePlayersToFront() { if (players_to_reorder.empty()) return; for (auto &player : players_to_reorder) { auto it = std::find(players_.begin(), players_.end(), player); if (it != players_.end() && it != players_.begin()) { std::shared_ptr dying_player = *it; players_.erase(it); players_.insert(players_.begin(), dying_player); } } players_to_reorder.clear(); } // Comprueba si está activo el menu de servicio para poner el juego en pausa void Game::checkServiceMenu() { if (demo_.enabled) return; static bool was_paused_before_service_menu_ = false; static bool service_menu_was_active_ = false; bool service_menu_is_active = ServiceMenu::get()->isEnabled(); if (service_menu_is_active && !service_menu_was_active_) { // El menú acaba de abrirse was_paused_before_service_menu_ = paused_; pause(true); } else if (!service_menu_is_active && service_menu_was_active_) { // El menú acaba de cerrarse pause(was_paused_before_service_menu_); } service_menu_was_active_ = service_menu_is_active; } #ifdef DEBUG // Comprueba los eventos en el modo DEBUG void Game::checkDebugEvents(const SDL_Event &event) { if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat == 0) { switch (event.key.key) { case SDLK_1: // Crea una powerball { // balloon_manager_->createPowerBall(); throwCoffee(players_.at(0)->getPosX() + (players_.at(0)->getWidth() / 2), players_.at(0)->getPosY() + (players_.at(0)->getHeight() / 2)); break; } case SDLK_2: // Activa o desactiva la aparición de globos { static bool deploy_balloons_ = true; deploy_balloons_ = !deploy_balloons_; balloon_manager_->enableBalloonDeployment(deploy_balloons_); break; } case SDLK_3: // Activa el modo para pasar el juego automaticamente { auto_pop_balloons_ = !auto_pop_balloons_; Notifier::get()->show({"auto advance: " + boolToString(auto_pop_balloons_)}); if (auto_pop_balloons_) { balloon_manager_->destroyAllBalloons(); playSound("power_ball_explosion.wav"); } balloon_manager_->enableBalloonDeployment(!auto_pop_balloons_); break; } case SDLK_4: // Suelta un item { createItem(ItemType::CLOCK, players_.at(0)->getPosX(), players_.at(0)->getPosY() - 40); break; } case SDLK_5: // 5.000 { const int X = players_.at(0)->getPosX() + (players_.at(0)->getWidth() - game_text_textures_[3]->getWidth()) / 2; createItemText(X, game_text_textures_.at(2)); break; } case SDLK_6: // Crea un mensaje { createMessage({paths_.at(0), paths_.at(1)}, Resource::get()->getTexture("game_text_get_ready")); break; } case SDLK_7: // 100.000 { const int X = players_.at(0)->getPosX() + (players_.at(0)->getWidth() - game_text_textures_[3]->getWidth()) / 2; createItemText(X, game_text_textures_.at(6)); break; } case SDLK_8: { for (const auto &player : players_) { if (player->isPlaying()) { createItem(ItemType::COFFEE_MACHINE, player->getPosX(), param.game.game_area.rect.y - Item::COFFEE_MACHINE_HEIGHT); break; } } break; } case SDLK_9: { tabe_->enable(); break; } default: break; } } } #endif