#include "game.hpp" #include // Para SDL_GetTicks, SDL_SetRenderTarget, SDL_EventType, SDL_CreateTexture, SDL_Delay, SDL_DestroyTexture, SDL_Event, SDL_GetRenderTarget, SDL_PollEvent, SDL_RenderTexture, SDL_SetTextureBlendMode, SDLK_1, SDLK_2, SDLK_3, SDLK_4, SDLK_5, SDLK_6, SDLK_7, SDLK_8, SDLK_9, SDLK_KP_MINUS, SDLK_KP_PLUS, SDL_BLENDMODE_BLEND, SDL_PixelFormat, SDL_Point, SDL_TextureAccess, Uint64 #include // Para find, clamp, find_if, min, shuffle #include // Para array #include // Para rand, size_t #include // Para function #include // Para size #include // Para shared_ptr, unique_ptr, __shared_ptr_access, make_unique, allocator, operator== #include // Para iota #include // Para optional #include // Para random_device, default_random_engine #include // Para move #include "asset.hpp" // Para Asset #include "audio.hpp" // Para Audio #include "background.hpp" // Para Background #include "balloon.hpp" // Para Balloon #include "balloon_manager.hpp" // Para BalloonManager #include "bullet.hpp" // Para Bullet #include "bullet_manager.hpp" // Para BulletManager #include "color.hpp" // Para Color, FLASH, NO_COLOR_MOD #include "difficulty.hpp" // Para Code #include "fade.hpp" // Para Fade #include "global_events.hpp" // Para handle #include "global_inputs.hpp" // Para check #include "hit.hpp" // Para Hit #include "input.hpp" // Para Input #include "input_types.hpp" // Para InputAction #include "item.hpp" // Para Item, ItemType #include "lang.hpp" // Para getText #include "manage_hiscore_table.hpp" // Para HiScoreEntry, ManageHiScoreTable #include "param.hpp" // Para Param, param, ParamGame, ParamScoreboard, ParamFade, ParamBalloon #include "path_sprite.hpp" // Para Path, PathSprite, PathType #include "pause_manager.hpp" // Para PauseManager #include "player.hpp" // Para Player #include "resource.hpp" // Para Resource #include "scoreboard.hpp" // Para Scoreboard #include "screen.hpp" // Para Screen #include "section.hpp" // Para Name, name, AttractMode, Options, attract_mode, options #include "smart_sprite.hpp" // Para SmartSprite #include "stage.hpp" // Para StageManager, StageData #include "tabe.hpp" // Para Tabe #include "text.hpp" // Para Text #include "texture.hpp" // Para Texture #include "ui/service_menu.hpp" // Para ServiceMenu #include "utils.hpp" // Para Zone, checkCollision, easeInQuint, easeOutQuint, boolToString #ifdef _DEBUG #include // Para basic_ostream, basic_ostream::operator<<, operator<<, cout #include "ui/notifier.hpp" // Para Notifier #endif // Constructor Game::Game(Player::Id player_id, int current_stage, bool demo_enabled) : renderer_(Screen::get()->getRenderer()), screen_(Screen::get()), input_(Input::get()), canvas_(SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.play_area.rect.w, param.game.play_area.rect.h)), pause_manager_(std::make_unique([this](bool is_paused) { onPauseStateChanged(is_paused); })), stage_manager_(std::make_unique()), balloon_manager_(std::make_unique(stage_manager_.get())), bullet_manager_(std::make_unique()), background_(std::make_unique(stage_manager_->getPowerNeededToReachStage(stage_manager_->getTotalStages() - 1))), fade_in_(std::make_unique()), fade_out_(std::make_unique()), tabe_(std::make_unique()), hit_(Hit(Resource::get()->getTexture("hit.png"))) { // Pasa variables demo_.enabled = demo_enabled; // Otras variables Section::name = Section::Name::GAME; Section::options = Section::Options::NONE; stage_manager_->initialize(Asset::get()->get("stages.txt")); stage_manager_->setPowerChangeCallback([this](int amount) { background_->incrementProgress(amount); }); stage_manager_->jumpToStage(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 ? static_cast(DEMO_FADE_PRE_DURATION_S * 1000) : 0); fade_in_->setPostDuration(0); fade_in_->setType(Fade::Type::RANDOM_SQUARE2); fade_in_->setMode(Fade::Mode::IN); #ifndef RECORDING fade_in_->activate(); #endif fade_out_->setColor(param.fade.color); fade_out_->setPostDuration(param.fade.post_duration_ms); fade_out_->setType(Fade::Type::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(); // Registra callbacks ServiceMenu::get()->setStateChangeCallback([this](bool is_active) { // Solo aplicar pausa si NO estamos en modo demo if (!demo_.enabled) { pause_manager_->setServiceMenuPause(is_active); } }); // Configura callbacks del BulletManager bullet_manager_->setTabeCollisionCallback([this](const std::shared_ptr& bullet) { return checkBulletTabeCollision(bullet); }); bullet_manager_->setBalloonCollisionCallback([this](const std::shared_ptr& bullet) { return checkBulletBalloonCollision(bullet); }); bullet_manager_->setOutOfBoundsCallback([this](const std::shared_ptr& bullet) { getPlayer(static_cast(bullet->getOwner()))->decScoreMultiplier(); }); #ifdef RECORDING setState(State::PLAYING); #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; if (Options::audio.enabled) { // Musica Audio::get()->stopMusic(); Audio::get()->setMusicVolume(Options::audio.music.volume); // Sonidos Audio::get()->stopAllSounds(); Audio::get()->setSoundVolume(Options::audio.sound.volume, Audio::Group::GAME); } } ServiceMenu::get()->setStateChangeCallback(nullptr); #ifdef RECORDING saveDemoFile(Asset::get()->get("demo1.bin"), demo_.data.at(0)); #endif Scoreboard::destroy(); SDL_DestroyTexture(canvas_); // Desregistra los jugadores de Options Options::keyboard.clearPlayers(); Options::gamepad_manager.clearPlayers(); } // Asigna texturas y animaciones void Game::setResources() { // Texturas - Game_text game_text_textures_.clear(); 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_.clear(); 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")); player_textures_.clear(); // Texturas - Player1 std::vector> player1_textures; player1_textures.emplace_back(Resource::get()->getTexture("player1_pal0")); player1_textures.emplace_back(Resource::get()->getTexture("player1_pal1")); player1_textures.emplace_back(Resource::get()->getTexture("player1_pal2")); player1_textures.emplace_back(Resource::get()->getTexture("player1_pal3")); player1_textures.emplace_back(Resource::get()->getTexture("player1_power.png")); player_textures_.push_back(player1_textures); // Texturas - Player2 std::vector> player2_textures; player2_textures.emplace_back(Resource::get()->getTexture("player2_pal0")); player2_textures.emplace_back(Resource::get()->getTexture("player2_pal1")); player2_textures.emplace_back(Resource::get()->getTexture("player2_pal2")); player2_textures.emplace_back(Resource::get()->getTexture("player2_pal3")); player2_textures.emplace_back(Resource::get()->getTexture("player2_power.png")); player_textures_.push_back(player2_textures); // Animaciones -- Jugador player_animations_.clear(); player_animations_.emplace_back(Resource::get()->getAnimation("player.ani")); player_animations_.emplace_back(Resource::get()->getAnimation("player_power.ani")); // Animaciones -- Items item_animations_.clear(); 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 if (!hi_score_achieved_) { hi_score_achieved_ = true; playSound("hi_score_achieved.wav"); // Emite un sonido createMessage({paths_.at(8), paths_.at(9)}, Resource::get()->getTexture("game_text_new_record")); // CRea un mensaje } } } } // Actualiza las variables del jugador void Game::updatePlayers(float delta_time) { for (auto& player : players_) { player->update(delta_time); 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_timer_ > 0) { balloon_manager_->popBalloon(balloon); } // En caso contrario, el jugador ha sido golpeado por un globo activo else { handlePlayerCollision(player, balloon); if (demo_.enabled && allPlayersAreNotPlaying()) { fade_out_->setType(Fade::Type::RANDOM_SQUARE2); fade_out_->setPostDuration(500); fade_out_->activate(); } } } // Comprueba las colisiones entre el jugador y los items checkPlayerItemCollision(player); } } // Organiza la lista de jugadores sortPlayersByZOrder(); } // Dibuja a los jugadores void Game::renderPlayers() { for (auto& player : players_) { player->render(); } } // Comprueba si hay cambio de fase y actualiza las variables void Game::updateStage() { if (!stage_manager_->isCurrentStageCompleted()) { return; // No hay cambio de fase } // Calcular poder sobrante antes de avanzar int power_overflow = 0; auto current_stage = stage_manager_->getCurrentStage(); if (current_stage.has_value()) { int current_power = stage_manager_->getCurrentPower(); int power_needed = current_stage->getPowerToComplete(); power_overflow = current_power - power_needed; // Poder que sobra } // Intentar avanzar a la siguiente fase if (!stage_manager_->advanceToNextStage()) { // No se pudo avanzar (probablemente juego completado) return; } // Aplicar el poder sobrante a la nueva fase if (power_overflow > 0) { stage_manager_->addPower(power_overflow); } // Efectos de cambio de fase playSound("stage_change.wav"); balloon_manager_->resetBalloonSpeed(); screen_->flash(Colors::FLASH, 0.05F); screen_->shake(); // Obtener datos de la nueva fase size_t current_stage_index = stage_manager_->getCurrentStageIndex(); size_t total_stages = stage_manager_->getTotalStages(); // Escribir texto por pantalla if (current_stage_index < total_stages) { // No es la última fase std::vector paths = {paths_.at(2), paths_.at(3)}; if (current_stage_index == total_stages - 1) { // Penúltima fase (será la última) createMessage(paths, Resource::get()->getTexture("game_text_last_stage")); } else { auto text = Resource::get()->getText("04b_25_2x_enhanced"); const std::string CAPTION = Lang::getText("[GAME_TEXT] 2") + std::to_string(total_stages - current_stage_index) + Lang::getText("[GAME_TEXT] 2A"); createMessage(paths, text->writeDXToTexture(Text::STROKE, CAPTION, -4, Colors::NO_COLOR_MOD, 1, param.game.item_text_outline_color)); } } // Modificar color de fondo en la última fase if (current_stage_index == total_stages - 1) { // Última fase background_->setColor(Color(0xdd, 0x19, 0x1d).DARKEN()); background_->setAlpha(96); } } // Actualiza las variables y sistemas durante el estado de fin de partida void Game::updateGameStateGameOver(float delta_time) { fade_out_->update(); updatePlayers(delta_time); updateScoreboard(delta_time); updateBackground(delta_time); balloon_manager_->update(delta_time); tabe_->update(delta_time); bullet_manager_->update(delta_time); updateItems(delta_time); updateSmartSprites(delta_time); updatePathSprites(delta_time); updateTimeStopped(delta_time); bullet_manager_->checkCollisions(); cleanLists(); if (game_over_timer_ < GAME_OVER_DURATION_S) { game_over_timer_ += delta_time; // Incremento time-based primero handleGameOverEvents(); // Maneja eventos después del incremento } if (Options::audio.enabled) { const float PROGRESS = std::min(game_over_timer_ / GAME_OVER_DURATION_S, 1.0F); const float VOL = 64.0F * (1.0F - PROGRESS); Audio::get()->setSoundVolume(static_cast(VOL), Audio::Group::GAME); } if (fade_out_->hasEnded()) { if (game_completed_timer_ > 0) { Section::name = Section::Name::CREDITS; // Los jugadores han completado el juego } else { Section::name = Section::Name::HI_SCORE_TABLE; // La partida ha terminado con la derrota de los jugadores } Section::options = Section::Options::HI_SCORE_AFTER_PLAYING; } } // Gestiona eventos para el estado del final del juego void Game::updateGameStateCompleted(float delta_time) { updatePlayers(delta_time); updateScoreboard(delta_time); updateBackground(delta_time); balloon_manager_->update(delta_time); tabe_->update(delta_time); bullet_manager_->update(delta_time); updateItems(delta_time); updateSmartSprites(delta_time); updatePathSprites(delta_time); cleanLists(); // Maneja eventos del juego completado handleGameCompletedEvents(); // Si los jugadores ya no estan y no quedan mensajes en pantalla if (allPlayersAreGameOver() && path_sprites_.empty()) { setState(State::GAME_OVER); } // Incrementa el acumulador al final game_completed_timer_ += delta_time; } // Comprueba el estado del juego void Game::checkState() { if (state_ != State::COMPLETED && stage_manager_->isGameCompleted()) { setState(State::COMPLETED); background_->setState(Background::State::COMPLETED); } if (state_ != State::GAME_OVER && allPlayersAreGameOver()) { setState(State::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 auto Game::checkPlayerBalloonCollision(std::shared_ptr& player) -> std::shared_ptr { #ifndef RECORDING 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 } } } #endif 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, Options::settings.hi_score_table.back().score); 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, Options::settings.hi_score_table.back().score); 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, Options::settings.hi_score_table.back().score); 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, Options::settings.hi_score_table.back().score); 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, Options::settings.hi_score_table.back().score); 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(); } } } } // Maneja la colisión entre bala y Tabe auto Game::checkBulletTabeCollision(const std::shared_ptr& bullet) -> bool { if (!tabe_->isEnabled()) { return false; } if (!checkCollision(bullet->getCollider(), tabe_->getCollider())) { return false; } tabe_->setState(Tabe::State::HIT); bullet->disable(); handleTabeHitEffects(); return true; } // Maneja los efectos de golpear al Tabe void Game::handleTabeHitEffects() { 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"); } } // Maneja la colisión entre bala y globos auto Game::checkBulletBalloonCollision(const std::shared_ptr& bullet) -> bool { for (auto& balloon : balloon_manager_->getBalloons()) { if (!balloon->isEnabled() || balloon->isInvulnerable()) { continue; } if (!checkCollision(balloon->getCollider(), bullet->getCollider())) { continue; } processBalloonHit(bullet, balloon); return true; } return false; } // Procesa el impacto en un globo void Game::processBalloonHit(const std::shared_ptr& bullet, const std::shared_ptr& balloon) { auto player = getPlayer(static_cast(bullet->getOwner())); handleItemDrop(balloon, player); handleBalloonDestruction(balloon, player); bullet->disable(); } // Maneja la caída de items cuando se destruye un globo void Game::handleItemDrop(const std::shared_ptr& balloon, const std::shared_ptr& player) { const auto DROPPED_ITEM = dropItem(); if (DROPPED_ITEM == ItemType::NONE || demo_.recording) { return; } if (DROPPED_ITEM != ItemType::COFFEE_MACHINE) { createItem(DROPPED_ITEM, balloon->getPosX(), balloon->getPosY()); playSound("item_drop.wav"); } else { createItem(DROPPED_ITEM, player->getPosX(), param.game.game_area.rect.y - Item::COFFEE_MACHINE_HEIGHT); coffee_machine_enabled_ = true; } } // Maneja la destrucción del globo y puntuación void Game::handleBalloonDestruction(const std::shared_ptr& balloon, const std::shared_ptr& player) { if (player->isPlaying()) { auto const SCORE = balloon_manager_->popBalloon(balloon) * player->getScoreMultiplier() * difficulty_score_multiplier_; player->addScore(SCORE, Options::settings.hi_score_table.back().score); player->incScoreMultiplier(); } setMenace(); updateHiScore(); } // Actualiza los items void Game::updateItems(float delta_time) { for (auto& item : items_) { if (item->isEnabled()) { item->update(delta_time); if (item->isOnFloor()) { playSound("title.wav"); screen_->shake(1, 0.033F, 0.067F); // desp=1, delay=0.033s, length=0.067s } } } } // Pinta los items activos void Game::renderItems() { for (auto& item : items_) { item->render(); } } // Devuelve un item al azar y luego segun sus probabilidades auto Game::dropItem() -> ItemType { 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::PACMAR; } 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])); } // Vacia el vector de items void Game::freeItems() { std::erase_if(items_, [&](const auto& item) { if (!item->isEnabled()) { // Comprobamos si hay que realizar una acción extra if (item->getType() == ItemType::COFFEE_MACHINE) { coffee_machine_enabled_ = false; } // Devolvemos 'true' para indicar que este elemento debe ser borrado. return true; } // Devolvemos 'false' para conservarlo. return false; }); } // Crea un objeto PathSprite void Game::createItemText(int x, const 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, 1.667F, easeOutQuint, 0); // 100 frames → 1.667s path_sprites_.back()->addPath(Y1, Y2, PathType::VERTICAL, x, 1.333F, easeInQuint, 0); // 80 frames → 1.333s path_sprites_.back()->enable(); } // Crea un objeto PathSprite void Game::createMessage(const std::vector& paths, const 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 la lista de smartsprites void Game::freeSmartSprites() { std::erase_if(smart_sprites_, [](const auto& sprite) { return sprite->hasFinished(); }); } // Vacia la lista de pathsprites void Game::freePathSprites() { std::erase_if(path_sprites_, [](const auto& sprite) { return sprite->hasFinished(); }); } // 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(Item::WIDTH); smart_sprites_.back()->setHeight(Item::HEIGHT); smart_sprites_.back()->setVelX((-1.0F + ((rand() % 5) * 0.5F)) * 60.0F); // Convertir a pixels/segundo smart_sprites_.back()->setVelY(-4.0F * 60.0F); // Convertir a pixels/segundo smart_sprites_.back()->setAccelX(0.0F); smart_sprites_.back()->setAccelY(0.2F * 60.0F * 60.0F); // Convertir a pixels/segundo² (0.2 × 60²) 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()->setFinishedDelay(0.0F); smart_sprites_.back()->setSpriteClip(0, Item::HEIGHT, Item::WIDTH, Item::HEIGHT); smart_sprites_.back()->setRotatingCenter({Item::WIDTH / 2, Item::HEIGHT / 2}); smart_sprites_.back()->setRotate(true); smart_sprites_.back()->setRotateAmount(90.0); } // Actualiza los SmartSprites void Game::updateSmartSprites(float delta_time) { for (auto& sprite : smart_sprites_) { sprite->update(delta_time); } } // Pinta los SmartSprites activos void Game::renderSmartSprites() { for (auto& sprite : smart_sprites_) { sprite->render(); } } // Actualiza los PathSprites void Game::updatePathSprites(float delta_time) { for (auto& sprite : path_sprites_) { sprite->update(delta_time); } } // 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, std::shared_ptr& balloon) { if (!player->isPlaying() || player->isInvulnerable()) { return; // Si no está jugando o tiene inmunidad, no hace nada } // Si tiene cafes if (player->hasExtraHit()) { // Lo pierde player->removeExtraHit(); throwCoffee(player->getPosX() + (Player::WIDTH / 2), player->getPosY() + (Player::HEIGHT / 2)); playSound("coffee_out.wav"); screen_->shake(); } else { // Si no tiene cafes, muere playSound("player_collision.wav"); if (param.game.hit_stop) { pauseMusic(); SDL_Delay(param.game.hit_stop_ms); resumeMusic(); } screen_->shake(); playSound("voice_no.wav"); player->setPlayingState(Player::State::ROLLING); sendPlayerToTheBack(player); if (allPlayersAreNotPlaying()) { stage_manager_->disablePowerCollection(); } } } // Actualiza el estado del tiempo detenido void Game::updateTimeStopped(float delta_time) { static constexpr float WARNING_THRESHOLD_S = 2.0F; // 120 frames a 60fps → segundos static constexpr float CLOCK_SOUND_INTERVAL_S = 0.5F; // 30 frames a 60fps → segundos static constexpr float COLOR_FLASH_INTERVAL_S = 0.25F; // 15 frames a 60fps → segundos if (time_stopped_timer_ > 0) { time_stopped_timer_ -= delta_time; // Fase de advertencia (últimos 2 segundos) if (time_stopped_timer_ <= WARNING_THRESHOLD_S) { static float last_sound_time_ = 0.0F; // CLAC al entrar en fase de advertencia if (!time_stopped_flags_.warning_phase_started) { playSound("clock.wav"); time_stopped_flags_.warning_phase_started = true; last_sound_time_ = 0.0F; // Reset para empezar el ciclo rápido } last_sound_time_ += delta_time; if (last_sound_time_ >= CLOCK_SOUND_INTERVAL_S) { balloon_manager_->normalColorsToAllBalloons(); playSound("clock.wav"); last_sound_time_ = 0.0F; time_stopped_flags_.color_flash_sound_played = false; // Reset flag para el próximo intervalo } else if (last_sound_time_ >= COLOR_FLASH_INTERVAL_S && !time_stopped_flags_.color_flash_sound_played) { balloon_manager_->reverseColorsToAllBalloons(); playSound("clock.wav"); time_stopped_flags_.color_flash_sound_played = true; // Evita que suene múltiples veces } } else { // Fase normal - solo sonido ocasional static float sound_timer_ = 0.0F; sound_timer_ += delta_time; if (sound_timer_ >= CLOCK_SOUND_INTERVAL_S) { playSound("clock.wav"); sound_timer_ = 0.0F; } } // Si el timer llega a 0 o menos, desactivar if (time_stopped_timer_ <= 0) { playSound("clock.wav"); // CLAC final disableTimeStopItem(); } } } // Actualiza toda la lógica del juego void Game::update(float delta_time) { screen_->update(delta_time); // Actualiza el objeto screen Audio::update(); // Actualiza el objeto audio updateDemo(delta_time); #ifdef RECORDING updateRecording(deltaTime); #endif updateGameStates(delta_time); fillCanvas(); } // Dibuja el juego void Game::render() { screen_->start(); // Prepara para empezar a dibujar en la textura de juego SDL_RenderTexture(renderer_, canvas_, nullptr, ¶m.game.play_area.rect); // Copia la textura con la zona de juego a la pantalla scoreboard_->render(); // Dibuja el marcador fade_in_->render(); // Dibuja el fade de entrada fade_out_->render(); // Dibuja el fade de salida // SDL_SetRenderDrawColor(renderer_, 255, 0, 0, 255); // SDL_RenderLine(renderer_, param.game.play_area.rect.x, param.game.play_area.center_y, param.game.play_area.rect.w, param.game.play_area.center_y); screen_->render(); // Vuelca el contenido del renderizador en pantalla } // Actualiza los estados del juego void Game::updateGameStates(float delta_time) { if (!pause_manager_->isPaused()) { switch (state_) { case State::FADE_IN: updateGameStateFadeIn(delta_time); break; case State::ENTERING_PLAYER: updateGameStateEnteringPlayer(delta_time); break; case State::SHOWING_GET_READY_MESSAGE: updateGameStateShowingGetReadyMessage(delta_time); break; case State::PLAYING: updateGameStatePlaying(delta_time); break; case State::COMPLETED: updateGameStateCompleted(delta_time); break; case State::GAME_OVER: updateGameStateGameOver(delta_time); break; default: break; } } } // Actualiza el fondo void Game::updateBackground(float delta_time) { background_->update(delta_time); } // Dibuja los elementos de la zona de juego en su textura void Game::fillCanvas() { // Dibuja el contenido de la zona de juego en su textura auto* temp = SDL_GetRenderTarget(renderer_); SDL_SetRenderTarget(renderer_, canvas_); // Dibuja los objetos background_->render(); balloon_manager_->render(); renderSmartSprites(); // El cafe que sale cuando te golpean renderItems(); tabe_->render(); bullet_manager_->render(); renderPlayers(); renderPathSprites(); // Deja el renderizador apuntando donde estaba SDL_SetRenderTarget(renderer_, temp); } // Habilita el efecto del item de detener el tiempo void Game::enableTimeStopItem() { balloon_manager_->stopAllBalloons(); balloon_manager_->reverseColorsToAllBalloons(); time_stopped_timer_ = TIME_STOPPED_DURATION_S; time_stopped_flags_.reset(); // Resetea flags al activar el item } // Deshabilita el efecto del item de detener el tiempo void Game::disableTimeStopItem() { time_stopped_timer_ = 0; balloon_manager_->startAllBalloons(); balloon_manager_->normalColorsToAllBalloons(); } // Calcula el deltatime en segundos auto Game::calculateDeltaTime() -> float { const Uint64 CURRENT_TIME = SDL_GetTicks(); const auto DELTA_TIME_MS = static_cast(CURRENT_TIME - last_time_); last_time_ = CURRENT_TIME; return DELTA_TIME_MS / 1000.0F; // Convertir de milisegundos a segundos } // Bucle para el juego void Game::run() { last_time_ = SDL_GetTicks(); while (Section::name == Section::Name::GAME) { const float DELTA_TIME = calculateDeltaTime(); checkInput(); update(DELTA_TIME); handleEvents(); // Tiene que ir antes del render render(); } } // Inicializa las variables que contienen puntos de ruta para mover objetos void Game::initPaths() { // --- Duración estándar para 80 frames --- // (Basado en tu ejemplo: 80 frames → 1.333s) const float DURATION_80F_S = 1.333F; // 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; // Y_base es la LÍNEA CENTRAL. addPath(true) restará H/2 const int Y_BASE = param.game.play_area.center_y; paths_.emplace_back(X0, X1, PathType::HORIZONTAL, Y_BASE, DURATION_80F_S, 0.5F, easeOutQuint); paths_.emplace_back(X1, X2, PathType::HORIZONTAL, Y_BASE, DURATION_80F_S, 0.0F, easeInQuint); } // Recorrido para el texto de "Last Stage!" o de "X stages left" (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; // X_base es la LÍNEA CENTRAL. addPath(true) restará W/2 const int X_BASE = param.game.play_area.center_x; paths_.emplace_back(Y0, Y1, PathType::VERTICAL, X_BASE, DURATION_80F_S, 0.5F, easeOutQuint); paths_.emplace_back(Y1, Y2, PathType::VERTICAL, X_BASE, DURATION_80F_S, 0.0F, easeInQuint); } // Recorrido para el texto de "Congratulations!!" (4,5) { 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; // Y_base es la LÍNEA CENTRAL. addPath(true) restará H/2 const int Y_BASE = param.game.play_area.center_y - (H / 2); paths_.emplace_back(X0, X1, PathType::HORIZONTAL, Y_BASE, DURATION_80F_S, 7.0F, easeOutQuint); paths_.emplace_back(X1, X2, PathType::HORIZONTAL, Y_BASE, DURATION_80F_S, 0.0F, easeInQuint); } // Recorrido para el texto de "1.000.000 points!" (6,7) { 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; // Y_base es la LÍNEA CENTRAL (desplazada PREV_H hacia abajo). addPath(true) restará H/2 const int Y_BASE = param.game.play_area.center_y + (H / 2); paths_.emplace_back(X0, X1, PathType::HORIZONTAL, Y_BASE, DURATION_80F_S, 7.0F, easeOutQuint); paths_.emplace_back(X1, X2, PathType::HORIZONTAL, Y_BASE, DURATION_80F_S, 0.0F, easeInQuint); } // Recorrido para el texto de "New Record!" (8,9) { const auto& texture = Resource::get()->getTexture("game_text_new_record"); 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); // Destino (no-fijo), está bien const int X2 = param.game.play_area.rect.w; // Y_base es la LÍNEA CENTRAL (desplazada 2*H hacia arriba). addPath(true) restará H/2 const int Y_BASE = param.game.play_area.center_y - (H * 2); paths_.emplace_back(X0, X1, PathType::HORIZONTAL, Y_BASE, DURATION_80F_S, 1.0F, easeOutQuint); paths_.emplace_back(X1, X2, PathType::HORIZONTAL, Y_BASE, DURATION_80F_S, 0.0F, easeInQuint); } // Recorrido para el texto de "Game Over" (10,11) { const auto& texture = Resource::get()->getTexture("game_text_game_over"); 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); // Destino (no-fijo), está bien const int Y2 = -H; // X_base es la LÍNEA CENTRAL. addPath(true) restará W/2 const int X_BASE = param.game.play_area.center_x; paths_.emplace_back(Y0, Y1, PathType::VERTICAL, X_BASE, DURATION_80F_S, 2.0F, easeOutQuint); paths_.emplace_back(Y1, Y2, PathType::VERTICAL, X_BASE, DURATION_80F_S, 0.0F, easeInQuint); } } // Actualiza las variables de ayuda void Game::updateHelper() { // Solo ofrece ayuda cuando la amenaza es elevada if (menace_ > 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 auto Game::allPlayersAreWaitingOrGameOver() -> bool { auto success = true; for (const auto& player : players_) { success &= player->isWaiting() || player->isGameOver(); } return success; } // Comprueba si todos los jugadores han terminado de jugar auto Game::allPlayersAreGameOver() -> bool { auto success = true; for (const auto& player : players_) { success &= player->isGameOver(); } return success; } // Comprueba si todos los jugadores han terminado de jugar auto Game::allPlayersAreNotPlaying() -> bool { auto success = true; for (const auto& player : players_) { success &= !player->isPlaying(); } return success; } // Comprueba los eventos que hay en cola void Game::handleEvents() { SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_EVENT_WINDOW_FOCUS_LOST: { pause_manager_->setFocusLossPause(!demo_.enabled); break; } case SDL_EVENT_WINDOW_FOCUS_GAINED: { pause_manager_->setFocusLossPause(false); break; } default: break; } #ifdef _DEBUG handleDebugEvents(event); #endif GlobalEvents::handle(event); } } // Actualiza el marcador void Game::updateScoreboard(float delta_time) { for (const auto& player : players_) { scoreboard_->setScore(player->getScoreBoardPanel(), player->getScore()); scoreboard_->setMult(player->getScoreBoardPanel(), player->getScoreMultiplier()); } // Resto de marcador scoreboard_->setStage(stage_manager_->getCurrentStageIndex() + 1); scoreboard_->setPower(stage_manager_->getCurrentStageProgressFraction()); scoreboard_->setHiScore(hi_score_.score); scoreboard_->setHiScoreName(hi_score_.name); scoreboard_->update(delta_time); } // Pone en el marcador el nombre del primer jugador de la tabla void Game::updateHiScoreName() { 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(Player::State::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(Player::State::GAME_OVER); } } // Comprobar estado de ambos jugadores checkAndUpdatePlayerStatus(0, 1); checkAndUpdatePlayerStatus(1, 0); } // Obtiene un jugador a partir de su "id" auto Game::getPlayer(Player::Id id) -> std::shared_ptr { auto it = std::ranges::find_if(players_, [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 auto Game::getController(Player::Id player_id) -> int { switch (player_id) { case Player::Id::PLAYER1: return 0; case Player::Id::PLAYER2: return 1; default: 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 ? demoHandlePassInput() : handlePlayersInput(); } // Mueve los jugadores en el modo demo if (demo_.enabled) { demoHandleInput(); } // 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 (const auto& gamepad : input_->getGamepads()) { if (input_->checkAction(Input::Action::PAUSE, Input::DO_NOT_ALLOW_REPEAT, Input::DO_NOT_CHECK_KEYBOARD, gamepad)) { pause_manager_->togglePlayerPause(); return; } } // Comprueba el teclado if (input_->checkAction(Input::Action::PAUSE, Input::DO_NOT_ALLOW_REPEAT, Input::CHECK_KEYBOARD)) { pause_manager_->togglePlayerPause(); return; } } // Gestiona las entradas de los jugadores en el modo demo para saltarse la demo. void Game::demoHandlePassInput() { 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::demoHandleInput() { for (const auto& player : players_) { if (player->isPlaying()) { demoHandlePlayerInput(player, player->getDemoFile()); // Maneja el input específico del jugador en modo demo. } } } // Procesa las entradas para un jugador específico durante el modo demo. void Game::demoHandlePlayerInput(const std::shared_ptr& player, int index) { const auto& demo_data = demo_.data.at(index).at(demo_.index); if (demo_data.left == 1) { player->setInput(Input::Action::LEFT); } else if (demo_data.right == 1) { player->setInput(Input::Action::RIGHT); } else if (demo_data.no_input == 1) { player->setInput(Input::Action::NONE); } if (demo_data.fire == 1) { handleFireInput(player, Bullet::Type::UP); } else if (demo_data.fire_left == 1) { handleFireInput(player, Bullet::Type::LEFT); } else if (demo_data.fire_right == 1) { handleFireInput(player, Bullet::Type::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, Bullet::Type type) { if (player->canFire()) { SDL_Point bullet = {0, 0}; switch (type) { case Bullet::Type::UP: player->setInput(Input::Action::FIRE_CENTER); bullet.x = 2 + player->getPosX() + (Player::WIDTH - Bullet::WIDTH) / 2; bullet.y = player->getPosY() - (Bullet::HEIGHT / 2); break; case Bullet::Type::LEFT: player->setInput(Input::Action::FIRE_LEFT); bullet.x = player->getPosX() - (Bullet::WIDTH / 2); bullet.y = player->getPosY(); break; case Bullet::Type::RIGHT: player->setInput(Input::Action::FIRE_RIGHT); bullet.x = player->getPosX() + Player::WIDTH - (Bullet::WIDTH / 2); bullet.y = player->getPosY(); break; default: break; } bullet_manager_->createBullet(bullet.x, bullet.y, type, player->getNextBulletColor(), static_cast(player->getId())); playSound(player->getBulletSoundFile()); // 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->startFiringSystem(cant_fire_counter); // Sistema de disparo de dos líneas } } // 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()) { handleNormalPlayerInput(player); // Maneja el input de los jugadores en modo normal } else if (player->isContinue()) { handlePlayerContinueInput(player); // Gestiona la continuación del jugador } else if (player->isWaiting()) { handlePlayerWaitingInput(player); // Gestiona la (re)entrada del jugador } else if (player->isEnteringName() || player->isEnteringNameGameCompleted() || player->isShowingName()) { handleNameInput(player); // Gestiona la introducción del nombre del jugador } } } // Maneja las entradas de movimiento y disparo para un jugador en modo normal. void Game::handleNormalPlayerInput(const std::shared_ptr& player) { if (input_->checkAction(Input::Action::LEFT, Input::ALLOW_REPEAT, player->getUsesKeyboard(), player->getGamepad())) { player->setInput(Input::Action::LEFT); #ifdef RECORDING demo_.keys.left = 1; #endif } else if (input_->checkAction(Input::Action::RIGHT, Input::ALLOW_REPEAT, player->getUsesKeyboard(), player->getGamepad())) { player->setInput(Input::Action::RIGHT); #ifdef RECORDING demo_.keys.right = 1; #endif } else { player->setInput(Input::Action::NONE); #ifdef RECORDING demo_.keys.no_input = 1; #endif } const bool AUTOFIRE = player->isPowerUp() || Options::settings.autofire; handleFireInputs(player, AUTOFIRE); // 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) { if (!player) { return; } if (input_->checkAction(Input::Action::FIRE_CENTER, autofire, player->getUsesKeyboard(), player->getGamepad())) { handleFireInput(player, Bullet::Type::UP); #ifdef RECORDING demo_.keys.fire = 1; #endif return; } if (input_->checkAction(Input::Action::FIRE_LEFT, autofire, player->getUsesKeyboard(), player->getGamepad())) { handleFireInput(player, Bullet::Type::LEFT); #ifdef RECORDING demo_.keys.fire_left = 1; #endif return; } if (input_->checkAction(Input::Action::FIRE_RIGHT, autofire, player->getUsesKeyboard(), player->getGamepad())) { handleFireInput(player, Bullet::Type::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::handlePlayerContinueInput(const std::shared_ptr& player) { if (input_->checkAction(Input::Action::START, Input::DO_NOT_ALLOW_REPEAT, player->getUsesKeyboard(), player->getGamepad())) { player->setPlayingState(Player::State::RECOVER); player->addCredit(); sendPlayerToTheFront(player); return; } // Disminuye el contador de continuación si se presiona cualquier botón de disparo. if (input_->checkAction(Input::Action::FIRE_LEFT, Input::DO_NOT_ALLOW_REPEAT, player->getUsesKeyboard(), player->getGamepad()) || input_->checkAction(Input::Action::FIRE_CENTER, Input::DO_NOT_ALLOW_REPEAT, player->getUsesKeyboard(), player->getGamepad()) || input_->checkAction(Input::Action::FIRE_RIGHT, Input::DO_NOT_ALLOW_REPEAT, player->getUsesKeyboard(), player->getGamepad())) { if (player->getContinueCounter() < param.scoreboard.skip_countdown_value) { player->decContinueCounter(); } } } // Maneja la continuación del jugador cuando no está jugando, permitiendo que continúe si se pulsa el botón de inicio. void Game::handlePlayerWaitingInput(const std::shared_ptr& player) { if (input_->checkAction(Input::Action::START, Input::DO_NOT_ALLOW_REPEAT, player->getUsesKeyboard(), player->getGamepad())) { player->setPlayingState(Player::State::ENTERING_SCREEN); player->addCredit(); sendPlayerToTheFront(player); } } // Procesa las entradas para la introducción del nombre del jugador. void Game::handleNameInput(const std::shared_ptr& player) { if (input_->checkAction(Input::Action::FIRE_LEFT, Input::DO_NOT_ALLOW_REPEAT, player->getUsesKeyboard(), player->getGamepad())) { if (player->isShowingName()) { player->passShowingName(); return; } player->setInput(Input::Action::FIRE_LEFT); playSound("service_menu_select.wav"); return; } if (input_->checkAction(Input::Action::FIRE_CENTER, Input::DO_NOT_ALLOW_REPEAT, player->getUsesKeyboard(), player->getGamepad()) || input_->checkAction(Input::Action::FIRE_RIGHT, Input::DO_NOT_ALLOW_REPEAT, player->getUsesKeyboard(), player->getGamepad())) { if (player->isShowingName()) { player->passShowingName(); return; } player->setInput(Input::Action::FIRE_CENTER); playSound("service_menu_back.wav"); return; } if (input_->checkAction(Input::Action::LEFT, Input::DO_NOT_ALLOW_REPEAT, player->getUsesKeyboard(), player->getGamepad())) { player->setInput(Input::Action::LEFT); playSound("service_menu_move.wav"); return; } if (input_->checkAction(Input::Action::RIGHT, Input::DO_NOT_ALLOW_REPEAT, player->getUsesKeyboard(), player->getGamepad())) { player->setInput(Input::Action::RIGHT); playSound("service_menu_move.wav"); return; } if (input_->checkAction(Input::Action::START, Input::DO_NOT_ALLOW_REPEAT, player->getUsesKeyboard(), player->getGamepad())) { if (player->isShowingName()) { player->passShowingName(); return; } player->setInput(Input::Action::START); player->setPlayingState(Player::State::SHOWING_NAME); playSound("name_input_accept.wav"); updateHiScoreName(); } } // Inicializa las variables para el modo DEMO void Game::initDemo(Player::Id player_id) { #ifdef RECORDING // En modo grabación, inicializar vector vacío para almacenar teclas demo_.data.emplace_back(); // Vector vacío para grabación demo_.data.at(0).reserve(TOTAL_DEMO_DATA); // Reservar espacio para 2000 elementos #endif if (demo_.enabled) { // Cambia el estado del juego setState(State::PLAYING); #ifndef RECORDING // En modo juego: cargar todas las demos y asignar una diferente a cada jugador auto const NUM_DEMOS = Asset::get()->getListByType(Asset::Type::DEMODATA).size(); for (size_t num_demo = 0; num_demo < NUM_DEMOS; ++num_demo) { demo_.data.emplace_back(Resource::get()->getDemoData(num_demo)); } // Crear índices mezclados para asignación aleatoria std::vector demo_indices(NUM_DEMOS); std::iota(demo_indices.begin(), demo_indices.end(), 0); std::random_device rd; std::default_random_engine rng(rd()); std::shuffle(demo_indices.begin(), demo_indices.end(), rng); // Asignar demos a jugadores (round-robin si hay más jugadores que demos) for (size_t i = 0; i < players_.size(); ++i) { size_t demo_index = demo_indices[i % NUM_DEMOS]; players_.at(i)->setDemoFile(demo_index); } #endif // Selecciona una pantalla al azar constexpr auto NUM_STAGES = 3; const auto STAGE = rand() % NUM_STAGES; constexpr std::array STAGES = {0.005F, 0.32F, 0.53F}; stage_manager_->setTotalPower(stage_manager_->getTotalPowerNeededToCompleteGame() * STAGES.at(STAGE)); background_->setProgress(stage_manager_->getTotalPower()); // Activa o no al otro jugador if (rand() % 3 != 0) { const auto OTHER_PLAYER_ID = player_id == Player::Id::PLAYER1 ? Player::Id::PLAYER2 : Player::Id::PLAYER1; auto other_player = getPlayer(OTHER_PLAYER_ID); other_player->setPlayingState(Player::State::PLAYING); } // Asigna cafes a los jugadores for (auto& player : players_) { if (player->isPlaying()) { for (int i = 0; i < rand() % 3; ++i) { player->giveExtraHit(); } } player->setInvulnerable(true); } // Configura los marcadores scoreboard_->setMode(Scoreboard::Id::LEFT, Scoreboard::Mode::DEMO); scoreboard_->setMode(Scoreboard::Id::RIGHT, Scoreboard::Mode::DEMO); // Silencia los globos balloon_manager_->setSounds(false); } // Modo grabar demo #ifdef RECORDING demo_.recording = true; #else demo_.recording = false; #endif demo_.index = 0; } // Inicializa el marcador void Game::initScoreboard() { scoreboard_->setPos(param.scoreboard.rect); scoreboard_->setMode(Scoreboard::Id::CENTER, Scoreboard::Mode::STAGE_INFO); for (const auto& player : players_) { scoreboard_->setName(player->getScoreBoardPanel(), player->getName()); if (player->isWaiting()) { scoreboard_->setMode(player->getScoreBoardPanel(), Scoreboard::Mode::WAITING); } } } // Inicializa las opciones relacionadas con la dificultad void Game::initDifficultyVars() { // Variables relacionadas con la dificultad switch (difficulty_) { case Difficulty::Code::EASY: { balloon_manager_->setDefaultBalloonSpeed(Balloon::GAME_TEMPO.at(0)); difficulty_score_multiplier_ = 0.5F; scoreboard_->setColor(param.scoreboard.easy_color); break; } case Difficulty::Code::NORMAL: { balloon_manager_->setDefaultBalloonSpeed(Balloon::GAME_TEMPO.at(0)); difficulty_score_multiplier_ = 1.0F; scoreboard_->setColor(param.scoreboard.normal_color); break; } case Difficulty::Code::HARD: { balloon_manager_->setDefaultBalloonSpeed(Balloon::GAME_TEMPO.at(4)); difficulty_score_multiplier_ = 1.5F; scoreboard_->setColor(param.scoreboard.hard_color); break; } default: break; } balloon_manager_->resetBalloonSpeed(); } // Inicializa los jugadores void Game::initPlayers(Player::Id player_id) { const int Y = param.game.play_area.rect.h - Player::HEIGHT; // Se hunde un pixel para esconder el outline de los pies const Player::State STATE = demo_.enabled ? Player::State::PLAYING : Player::State::ENTERING_SCREEN; // Crea al jugador uno y lo pone en modo espera Player::Config config_player1{ .id = Player::Id::PLAYER1, #ifdef RECORDING .x = param.game.play_area.center_x - (Player::WIDTH / 2), #else .x = param.game.play_area.first_quarter_x - (Player::WIDTH / 2), #endif .y = Y, .demo = demo_.enabled, .play_area = ¶m.game.play_area.rect, .texture = player_textures_.at(0), .animations = player_animations_, .hi_score_table = &Options::settings.hi_score_table, .glowing_entry = &Options::settings.glowing_entries.at(static_cast(Player::Id::PLAYER1) - 1), .stage_info = stage_manager_.get()}; auto player1 = std::make_unique(config_player1); player1->setBulletColors(Bullet::Color::YELLOW, Bullet::Color::GREEN); player1->setBulletSoundFile("bullet1p.wav"); player1->setScoreBoardPanel(Scoreboard::Id::LEFT); player1->setName(Lang::getText("[SCOREBOARD] 1")); player1->setGamepad(Options::gamepad_manager.getGamepad(Player::Id::PLAYER1).instance); player1->setUsesKeyboard(Player::Id::PLAYER1 == Options::keyboard.player_id); #ifdef RECORDING player1->setPlayingState(Player::State::PLAYING); #else player1->setPlayingState((player_id == Player::Id::BOTH_PLAYERS || player_id == Player::Id::PLAYER1) ? STATE : Player::State::WAITING); #endif // Crea al jugador dos y lo pone en modo espera Player::Config config_player2{ .id = Player::Id::PLAYER2, .x = param.game.play_area.third_quarter_x - (Player::WIDTH / 2), .y = Y, .demo = demo_.enabled, .play_area = ¶m.game.play_area.rect, .texture = player_textures_.at(1), .animations = player_animations_, .hi_score_table = &Options::settings.hi_score_table, .glowing_entry = &Options::settings.glowing_entries.at(static_cast(Player::Id::PLAYER2) - 1), .stage_info = stage_manager_.get()}; auto player2 = std::make_unique(config_player2); player2->setBulletColors(Bullet::Color::RED, Bullet::Color::PURPLE); player2->setBulletSoundFile("bullet2p.wav"); player2->setScoreBoardPanel(Scoreboard::Id::RIGHT); player2->setName(Lang::getText("[SCOREBOARD] 2")); player2->setGamepad(Options::gamepad_manager.getGamepad(Player::Id::PLAYER2).instance); player2->setUsesKeyboard(Player::Id::PLAYER2 == Options::keyboard.player_id); player2->setPlayingState((player_id == Player::Id::BOTH_PLAYERS || player_id == Player::Id::PLAYER2) ? STATE : Player::State::WAITING); // Añade los jugadores al vector de forma que el jugador 1 se pinte por delante del jugador 2 players_.push_back(std::move(player2)); players_.push_back(std::move(player1)); // Registra los jugadores en Options for (const auto& player : players_) { Options::keyboard.addPlayer(player); Options::gamepad_manager.addPlayer(player); } } // Hace sonar la música void Game::playMusic(const std::string& music_file, int loop) { Audio::get()->playMusic(music_file, loop); } // Pausa la música void Game::pauseMusic() { Audio::get()->pauseMusic(); } // Retoma la música que eestaba pausada void Game::resumeMusic() { Audio::get()->resumeMusic(); } // Detiene la música void Game::stopMusic() const { if (!demo_.enabled) { Audio::get()->stopMusic(); } } // Actualiza las variables durante el modo demo void Game::updateDemo(float delta_time) { if (demo_.enabled) { balloon_manager_->setCreationTimeEnabled(balloon_manager_->getNumBalloons() != 0); // Actualiza ambos fades fade_in_->update(); fade_out_->update(); // Actualiza el contador de tiempo y el índice demo_.elapsed_s += delta_time; demo_.index = static_cast(demo_.elapsed_s * 60.0F); // Activa el fundido antes de acabar con los datos de la demo if (demo_.index == TOTAL_DEMO_DATA - 200) { fade_out_->setType(Fade::Type::RANDOM_SQUARE2); fade_out_->setPostDuration(param.fade.post_duration_ms); 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(float deltaTime) { // Actualiza el contador de tiempo y el índice demo_.elapsed_s += deltaTime; demo_.index = static_cast(demo_.elapsed_s * 60.0F); if (demo_.index >= TOTAL_DEMO_DATA) { Section::name = Section::Name::QUIT; return; } // Almacenar las teclas del frame actual en el vector de grabación if (demo_.index < TOTAL_DEMO_DATA && demo_.data.size() > 0) { // Asegurar que el vector tenga el tamaño suficiente if (demo_.data.at(0).size() <= static_cast(demo_.index)) { demo_.data.at(0).resize(demo_.index + 1); } // Almacenar las teclas del frame actual demo_.data.at(0).at(demo_.index) = demo_.keys; // Resetear las teclas para el siguiente frame demo_.keys = DemoKeys(); } } #endif // Actualiza las variables durante dicho estado void Game::updateGameStateFadeIn(float delta_time) { fade_in_->update(); updateScoreboard(delta_time); updateBackground(delta_time); if (fade_in_->hasEnded()) { setState(State::ENTERING_PLAYER); balloon_manager_->createTwoBigBalloons(); setMenace(); } } // Actualiza las variables durante dicho estado void Game::updateGameStateEnteringPlayer(float delta_time) { balloon_manager_->update(delta_time); updatePlayers(delta_time); updateScoreboard(delta_time); updateBackground(delta_time); for (const auto& player : players_) { if (player->isPlaying()) { setState(State::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(float delta_time) { updateGameStatePlaying(delta_time); constexpr float MUSIC_START_S = 1.67F; static float music_timer_ = 0.0F; music_timer_ += delta_time; if (music_timer_ >= MUSIC_START_S) { playMusic("playing.ogg"); music_timer_ = 0.0F; setState(State::PLAYING); } } // Actualiza las variables durante el transcurso normal del juego void Game::updateGameStatePlaying(float delta_time) { #ifdef _DEBUG if (auto_pop_balloons_) { stage_manager_->addPower(2); } #endif updatePlayers(delta_time); checkPlayersStatusPlaying(); updateScoreboard(delta_time); updateBackground(delta_time); balloon_manager_->update(delta_time); tabe_->update(delta_time); bullet_manager_->update(delta_time); updateItems(delta_time); updateStage(); updateSmartSprites(delta_time); updatePathSprites(delta_time); updateTimeStopped(delta_time); updateHelper(); bullet_manager_->checkCollisions(); updateMenace(); checkAndUpdateBalloonSpeed(); checkState(); cleanLists(); } // Vacía los vectores de elementos deshabilitados void Game::cleanLists() { bullet_manager_->freeBullets(); balloon_manager_->freeBalloons(); freeItems(); freeSmartSprites(); freePathSprites(); } // Gestiona el nivel de amenaza void Game::updateMenace() { if (state_ != State::PLAYING) { return; } auto current_stage = stage_manager_->getCurrentStage(); if (!current_stage.has_value()) { return; } const auto& stage = current_stage.value(); const double FRACTION = stage_manager_->getCurrentStageProgressFraction(); const int DIFFERENCE = stage.getMaxMenace() - stage.getMinMenace(); // Aumenta el nivel de amenaza en función del progreso de la fase menace_threshold_ = stage.getMinMenace() + (DIFFERENCE * FRACTION); if (menace_ < menace_threshold_) { balloon_manager_->deployRandomFormation(stage_manager_->getCurrentStageIndex()); setMenace(); } } // Calcula y establece el valor de amenaza en funcion de los globos activos void Game::setMenace() { menace_ = balloon_manager_->getMenace(); } // Actualiza la velocidad de los globos en funcion del poder acumulado de la fase void Game::checkAndUpdateBalloonSpeed() { if (difficulty_ != Difficulty::Code::NORMAL) { return; } const float PERCENT = stage_manager_->getCurrentStageProgressFraction(); constexpr std::array THRESHOLDS = {0.2F, 0.4F, 0.6F, 0.8F}; for (size_t i = 0; i < std::size(THRESHOLDS); ++i) { // Si la velocidad actual del globo es la correspondiente al umbral "i" y el porcentaje de progreso ha superado ese umbral if (balloon_manager_->getBalloonSpeed() == Balloon::GAME_TEMPO.at(i) && PERCENT > THRESHOLDS.at(i)) { // Sube la velocidad al siguiente nivel (i + 1) balloon_manager_->setBalloonSpeed(Balloon::GAME_TEMPO.at(i + 1)); return; } } } // Cambia el estado del juego void Game::setState(State state) { state_ = state; counter_ = 0; switch (state) { case State::COMPLETED: // Para la música y elimina todos los globos e items stopMusic(); // Detiene la música balloon_manager_->destroyAllBalloons(); // Destruye a todos los globos playSound("power_ball_explosion.wav"); // Sonido de destruir todos los globos destroyAllItems(); // Destruye todos los items background_->setAlpha(0); // Elimina el tono rojo de las últimas pantallas tabe_->disableSpawning(); // Deshabilita la creacion de Tabes game_completed_flags_.reset(); // Resetea flags de juego completado break; case State::GAME_OVER: game_over_flags_.reset(); // Resetea flags de game over break; default: break; } } void Game::playSound(const std::string& name) const { 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::sortPlayersByZOrder() { // Procesar jugadores que van al fondo (se dibujan primero) if (!players_to_put_at_back_.empty()) { for (auto& player : players_to_put_at_back_) { auto it = std::ranges::find(players_, player); if (it != players_.end() && it != players_.begin()) { const std::shared_ptr& dying_player = *it; players_.erase(it); players_.insert(players_.begin(), dying_player); } } players_to_put_at_back_.clear(); } // Procesar jugadores que van al frente (se dibujan últimos) if (!players_to_put_at_front_.empty()) { for (auto& player : players_to_put_at_front_) { auto it = std::ranges::find(players_, player); if (it != players_.end() && it != players_.end() - 1) { const std::shared_ptr& front_player = *it; players_.erase(it); players_.push_back(front_player); } } players_to_put_at_front_.clear(); } } // Mueve el jugador para pintarlo al fondo de la lista de jugadores void Game::sendPlayerToTheBack(const std::shared_ptr& player) { players_to_put_at_back_.push_back(player); } // Mueve el jugador para pintarlo el primero de la lista de jugadores void Game::sendPlayerToTheFront(const std::shared_ptr& player) { players_to_put_at_front_.push_back(player); } void Game::onPauseStateChanged(bool is_paused) { screen_->attenuate(is_paused); tabe_->pauseTimer(is_paused); } // Maneja eventos del juego completado usando flags para triggers únicos void Game::handleGameCompletedEvents() { constexpr float START_CELEBRATIONS_S = 6.0F; constexpr float END_CELEBRATIONS_S = 14.0F; // Inicio de celebraciones if (!game_completed_flags_.start_celebrations_triggered && game_completed_timer_ >= START_CELEBRATIONS_S) { 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, Options::settings.hi_score_table.back().score); player->setPlayingState(Player::State::CELEBRATING); } else { player->setPlayingState(Player::State::GAME_OVER); } } updateHiScore(); playMusic("congratulations.ogg", 1); game_completed_flags_.start_celebrations_triggered = true; } // Fin de celebraciones if (!game_completed_flags_.end_celebrations_triggered && game_completed_timer_ >= END_CELEBRATIONS_S) { for (auto& player : players_) { if (player->isCelebrating()) { player->setPlayingState(player->qualifiesForHighScore() ? Player::State::ENTERING_NAME_GAME_COMPLETED : Player::State::LEAVING_SCREEN); } } game_completed_flags_.end_celebrations_triggered = true; } } // Maneja eventos discretos basados en tiempo durante el estado game over void Game::handleGameOverEvents() { constexpr float MESSAGE_TRIGGER_S = 1.5F; constexpr float FADE_TRIGGER_S = GAME_OVER_DURATION_S - 2.5F; // Trigger inicial: fade out de música y sonidos de globos if (!game_over_flags_.music_fade_triggered) { Audio::get()->fadeOutMusic(1000); balloon_manager_->setBouncingSounds(true); game_over_flags_.music_fade_triggered = true; } // Trigger del mensaje "Game Over" if (!game_over_flags_.message_triggered && game_over_timer_ >= MESSAGE_TRIGGER_S) { createMessage({paths_.at(10), paths_.at(11)}, Resource::get()->getTexture("game_text_game_over")); playSound("voice_game_over.wav"); game_over_flags_.message_triggered = true; } // Trigger del fade out if (!game_over_flags_.fade_out_triggered && game_over_timer_ >= FADE_TRIGGER_S && !fade_out_->isEnabled()) { fade_out_->activate(); game_over_flags_.fade_out_triggered = true; } } #ifdef _DEBUG // Comprueba los eventos en el modo DEBUG void Game::handleDebugEvents(const SDL_Event& event) { static int formation_id_ = 0; if (event.type == SDL_EVENT_KEY_DOWN && static_cast(event.key.repeat) == 0) { switch (event.key.key) { case SDLK_1: { // Crea una powerball balloon_manager_->createPowerBall(); 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() + ((Player::WIDTH - 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() + ((Player::WIDTH - 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); coffee_machine_enabled_ = true; break; } } break; } case SDLK_9: { tabe_->enable(); break; } case SDLK_KP_PLUS: { ++formation_id_; balloon_manager_->destroyAllBalloons(); balloon_manager_->deployFormation(formation_id_); std::cout << formation_id_ << '\n'; break; } case SDLK_KP_MINUS: { --formation_id_; balloon_manager_->destroyAllBalloons(); balloon_manager_->deployFormation(formation_id_); std::cout << formation_id_ << '\n'; break; } default: break; } } } #endif