2081 lines
78 KiB
C++
2081 lines
78 KiB
C++
#include "game.h"
|
||
|
||
#include <SDL3/SDL.h> // Para SDL_GetTicks, SDL_SetRenderTarget, SDL_CreateTexture, SDL_Delay, SDL_DestroyTexture, SDL_EventType, SDL_GetRenderTarget, SDL_PollEvent, SDL_RenderTexture, SDL_SetTextureBlendMode, SDL_BLENDMODE_BLEND, SDL_Event, SDL_PixelFormat, SDL_Point, SDL_TextureAccess
|
||
|
||
#include <algorithm> // Para find, clamp, find_if, min, std::shuffle, std::iota
|
||
#include <array> // Para array
|
||
#include <cstdlib> // Para rand, size_t
|
||
#include <functional> // Para function
|
||
#include <iterator> // Para size
|
||
#include <memory> // Para shared_ptr, unique_ptr, __shared_ptr_access, allocator, make_unique, operator==, make_shared
|
||
#include <random> // std::random_device, std::default_random_engine
|
||
#include <utility> // Para move
|
||
|
||
#include "asset.h" // Para Asset
|
||
#include "audio.h" // Para Audio
|
||
#include "background.h" // Para Background
|
||
#include "balloon.h" // Para Balloon
|
||
#include "balloon_manager.h" // Para BalloonManager
|
||
#include "bullet.h" // Para Bullet, Bullet::Type, BulletMoveStatus
|
||
#include "bullet_manager.h" // Para BulletManager
|
||
#include "color.h" // Para Color, Colors::FLASH
|
||
#include "difficulty.h" // Para Code
|
||
#include "fade.h" // Para Fade, FadeType, FadeMode
|
||
#include "global_events.h" // Para check
|
||
#include "global_inputs.h" // Para check
|
||
#include "hit.h" // Para Hit
|
||
#include "input.h" // Para Input
|
||
#include "input_types.h" // Para InputAction
|
||
#include "item.h" // Para Item, ItemType
|
||
#include "lang.h" // Para getText
|
||
#include "manage_hiscore_table.h" // Para HiScoreEntry, ManageHiScoreTable
|
||
#include "param.h" // Para Param, param, ParamGame, ParamScoreboard, ParamFade, ParamBalloon
|
||
#include "path_sprite.h" // Para Path, PathSprite, createPath, PathType
|
||
#include "pause_manager.h" // Para PauseManager
|
||
#include "player.h" // Para Player
|
||
#include "resource.h" // Para Resource
|
||
#include "scoreboard.h" // Para Scoreboard
|
||
#include "screen.h" // Para Screen
|
||
#include "section.hpp" // Para Name, name, AttractMode, Options, attract_mode, options
|
||
#include "smart_sprite.h" // Para SmartSprite
|
||
#include "stage.h" // Para number, Stage, get, total_power, power, init, power_can_be_added, stages
|
||
#include "tabe.h" // Para Tabe
|
||
#include "text.h" // Para Text
|
||
#include "texture.h" // Para Texture
|
||
#include "ui/service_menu.h" // Para ServiceMenu
|
||
#ifdef _DEBUG
|
||
#include <iostream> // Para std::cout
|
||
|
||
#include "ui/notifier.h" // 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<PauseManager>([this](bool is_paused) { onPauseStateChanged(is_paused); })),
|
||
stage_manager_(std::make_unique<StageManager>()),
|
||
balloon_manager_(std::make_unique<BalloonManager>(stage_manager_.get())),
|
||
bullet_manager_(std::make_unique<BulletManager>()),
|
||
background_(std::make_unique<Background>(stage_manager_->getPowerNeededToReachStage(stage_manager_->getTotalStages() - 1))),
|
||
fade_in_(std::make_unique<Fade>()),
|
||
fade_out_(std::make_unique<Fade>()),
|
||
tabe_(std::make_unique<Tabe>()),
|
||
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<int>(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>& bullet) {
|
||
return checkBulletTabeCollision(bullet);
|
||
});
|
||
|
||
bullet_manager_->setBalloonCollisionCallback([this](const std::shared_ptr<Bullet>& bullet) {
|
||
return checkBulletBalloonCollision(bullet);
|
||
});
|
||
|
||
bullet_manager_->setOutOfBoundsCallback([this](const std::shared_ptr<Bullet>& bullet) {
|
||
getPlayer(static_cast<Player::Id>(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<ManageHiScoreTable>(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<std::shared_ptr<Texture>> 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<std::shared_ptr<Texture>> 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 deltaTime) {
|
||
for (auto& player : players_) {
|
||
player->update(deltaTime);
|
||
|
||
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<Path> 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 deltaTime) {
|
||
fade_out_->update();
|
||
updatePlayers(deltaTime);
|
||
updateScoreboard(deltaTime);
|
||
updateBackground(deltaTime);
|
||
balloon_manager_->update(deltaTime);
|
||
tabe_->update(deltaTime);
|
||
bullet_manager_->update(deltaTime);
|
||
updateItems(deltaTime);
|
||
updateSmartSprites(deltaTime);
|
||
updatePathSprites(deltaTime);
|
||
updateTimeStopped(deltaTime);
|
||
bullet_manager_->checkCollisions();
|
||
cleanVectors();
|
||
|
||
if (game_over_timer_ < GAME_OVER_DURATION_S) {
|
||
game_over_timer_ += deltaTime; // 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<int>(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 deltaTime) {
|
||
updatePlayers(deltaTime);
|
||
updateScoreboard(deltaTime);
|
||
updateBackground(deltaTime);
|
||
balloon_manager_->update(deltaTime);
|
||
tabe_->update(deltaTime);
|
||
bullet_manager_->update(deltaTime);
|
||
updateItems(deltaTime);
|
||
updateSmartSprites(deltaTime);
|
||
updatePathSprites(deltaTime);
|
||
cleanVectors();
|
||
|
||
// 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_ += deltaTime;
|
||
}
|
||
|
||
// 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>& player) -> std::shared_ptr<Balloon> {
|
||
#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>& 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>& 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>& 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>& bullet, const std::shared_ptr<Balloon>& balloon) {
|
||
auto player = getPlayer(static_cast<Player::Id>(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>& balloon, const std::shared_ptr<Player>& 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(std::shared_ptr<Balloon> balloon, const std::shared_ptr<Player>& player) {
|
||
if (player->isPlaying()) {
|
||
auto const SCORE = balloon_manager_->popBalloon(std::move(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 deltaTime) {
|
||
for (auto& item : items_) {
|
||
if (item->isEnabled()) {
|
||
item->update(deltaTime);
|
||
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::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<Item>(type, x, y, param.game.play_area.rect, item_textures_[static_cast<int>(type) - 1], item_animations_[static_cast<int>(type) - 1]));
|
||
}
|
||
|
||
// 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, const std::shared_ptr<Texture>& texture) {
|
||
path_sprites_.emplace_back(std::make_unique<PathSprite>(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<int>(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<float>(W), static_cast<float>(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<Path>& paths, const std::shared_ptr<Texture>& texture) {
|
||
path_sprites_.emplace_back(std::make_unique<PathSprite>(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<SmartSprite>(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 deltaTime) {
|
||
for (auto& sprite : smart_sprites_) {
|
||
sprite->update(deltaTime);
|
||
}
|
||
}
|
||
|
||
// Pinta los SmartSprites activos
|
||
void Game::renderSmartSprites() {
|
||
for (auto& sprite : smart_sprites_) {
|
||
sprite->render();
|
||
}
|
||
}
|
||
|
||
// Actualiza los PathSprites
|
||
void Game::updatePathSprites(float deltaTime) {
|
||
for (auto& sprite : path_sprites_) {
|
||
sprite->update(deltaTime);
|
||
}
|
||
}
|
||
|
||
// 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>& player, std::shared_ptr<Balloon>& 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 deltaTime) {
|
||
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_ -= deltaTime;
|
||
|
||
// 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 += deltaTime;
|
||
|
||
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 += deltaTime;
|
||
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 deltaTime) {
|
||
screen_->update(deltaTime); // Actualiza el objeto screen
|
||
Audio::update(); // Actualiza el objeto audio
|
||
|
||
updateDemo(deltaTime);
|
||
#ifdef RECORDING
|
||
updateRecording(deltaTime);
|
||
#endif
|
||
updateGameStates(deltaTime);
|
||
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
|
||
|
||
screen_->render(); // Vuelca el contenido del renderizador en pantalla
|
||
}
|
||
|
||
// Actualiza los estados del juego
|
||
void Game::updateGameStates(float deltaTime) {
|
||
if (!pause_manager_->isPaused()) {
|
||
switch (state_) {
|
||
case State::FADE_IN:
|
||
updateGameStateFadeIn(deltaTime);
|
||
break;
|
||
case State::ENTERING_PLAYER:
|
||
updateGameStateEnteringPlayer(deltaTime);
|
||
break;
|
||
case State::SHOWING_GET_READY_MESSAGE:
|
||
updateGameStateShowingGetReadyMessage(deltaTime);
|
||
break;
|
||
case State::PLAYING:
|
||
updateGameStatePlaying(deltaTime);
|
||
break;
|
||
case State::COMPLETED:
|
||
updateGameStateCompleted(deltaTime);
|
||
break;
|
||
case State::GAME_OVER:
|
||
updateGameStateGameOver(deltaTime);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Actualiza el fondo
|
||
void Game::updateBackground(float deltaTime) {
|
||
background_->update(deltaTime);
|
||
}
|
||
|
||
// 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 float delta_time_ms = static_cast<float>(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() {
|
||
// 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(createPath(X0, X1, PathType::HORIZONTAL, Y, 80, easeOutQuint), 0.5f);
|
||
paths_.emplace_back(createPath(X1, X2, PathType::HORIZONTAL, Y, 80, easeInQuint), 0);
|
||
}
|
||
|
||
// 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;
|
||
const int X = param.game.play_area.center_x;
|
||
paths_.emplace_back(createPath(Y0, Y1, PathType::VERTICAL, X, 80, easeOutQuint), 0.5f);
|
||
paths_.emplace_back(createPath(Y1, Y2, PathType::VERTICAL, X, 80, easeInQuint), 0);
|
||
}
|
||
|
||
// 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;
|
||
const int Y = param.game.play_area.center_y - (H / 2);
|
||
paths_.emplace_back(createPath(X0, X1, PathType::HORIZONTAL, Y, 80, easeOutQuint), 7.0F);
|
||
paths_.emplace_back(createPath(X1, X2, PathType::HORIZONTAL, Y, 80, easeInQuint), 0);
|
||
}
|
||
|
||
// 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;
|
||
const int Y = param.game.play_area.center_y + (H / 2);
|
||
paths_.emplace_back(createPath(X0, X1, PathType::HORIZONTAL, Y, 80, easeOutQuint), 7.0F);
|
||
paths_.emplace_back(createPath(X1, X2, PathType::HORIZONTAL, Y, 80, easeInQuint), 0);
|
||
}
|
||
|
||
// 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);
|
||
const int X2 = param.game.play_area.rect.w;
|
||
const int Y = param.game.play_area.center_y - (H / 2) - (H * 2);
|
||
paths_.emplace_back(createPath(X0, X1, PathType::HORIZONTAL, Y, 80, easeOutQuint), 1.0f);
|
||
paths_.emplace_back(createPath(X1, X2, PathType::HORIZONTAL, Y, 80, easeInQuint), 0);
|
||
}
|
||
|
||
// 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);
|
||
const int Y2 = -H;
|
||
const int X = param.game.play_area.center_x;
|
||
paths_.emplace_back(createPath(Y0, Y1, PathType::VERTICAL, X, 80, easeOutQuint), 2.0f);
|
||
paths_.emplace_back(createPath(Y1, Y2, PathType::VERTICAL, X, 80, easeInQuint), 0);
|
||
}
|
||
}
|
||
|
||
// 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 deltaTime) {
|
||
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(deltaTime);
|
||
}
|
||
|
||
// 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<Player> {
|
||
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
|
||
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>& 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>& 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<int>(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>& 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>& 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>& 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>& 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>& 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<size_t> 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<float, NUM_STAGES> 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 + 1; // 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<int>(Player::Id::PLAYER1) - 1),
|
||
.stage_info = stage_manager_.get()};
|
||
|
||
auto player1 = std::make_unique<Player>(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<int>(Player::Id::PLAYER2) - 1),
|
||
.stage_info = stage_manager_.get()};
|
||
|
||
auto player2 = std::make_unique<Player>(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 deltaTime) {
|
||
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 += deltaTime;
|
||
demo_.index = static_cast<int>(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<int>(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<size_t>(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 deltaTime) {
|
||
fade_in_->update();
|
||
updateScoreboard(deltaTime);
|
||
updateBackground(deltaTime);
|
||
if (fade_in_->hasEnded()) {
|
||
setState(State::ENTERING_PLAYER);
|
||
balloon_manager_->createTwoBigBalloons();
|
||
setMenace();
|
||
}
|
||
}
|
||
|
||
// Actualiza las variables durante dicho estado
|
||
void Game::updateGameStateEnteringPlayer(float deltaTime) {
|
||
balloon_manager_->update(deltaTime);
|
||
updatePlayers(deltaTime);
|
||
updateScoreboard(deltaTime);
|
||
updateBackground(deltaTime);
|
||
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 deltaTime) {
|
||
updateGameStatePlaying(deltaTime);
|
||
|
||
constexpr float MUSIC_START_S = 1.67F;
|
||
static float music_timer = 0.0f;
|
||
music_timer += deltaTime;
|
||
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 deltaTime) {
|
||
#ifdef _DEBUG
|
||
if (auto_pop_balloons_) {
|
||
stage_manager_->addPower(2);
|
||
}
|
||
#endif
|
||
updatePlayers(deltaTime);
|
||
checkPlayersStatusPlaying();
|
||
updateScoreboard(deltaTime);
|
||
updateBackground(deltaTime);
|
||
balloon_manager_->update(deltaTime);
|
||
tabe_->update(deltaTime);
|
||
bullet_manager_->update(deltaTime);
|
||
updateItems(deltaTime);
|
||
updateStage();
|
||
updateSmartSprites(deltaTime);
|
||
updatePathSprites(deltaTime);
|
||
updateTimeStopped(deltaTime);
|
||
updateHelper();
|
||
bullet_manager_->checkCollisions();
|
||
updateMenace();
|
||
checkAndUpdateBalloonSpeed();
|
||
checkState();
|
||
cleanVectors();
|
||
}
|
||
|
||
// Vacía los vectores de elementos deshabilitados
|
||
void Game::cleanVectors() {
|
||
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<float, 4> 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::find(players_.begin(), players_.end(), player);
|
||
if (it != players_.end() && it != players_.begin()) {
|
||
std::shared_ptr<Player> 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::find(players_.begin(), players_.end(), player);
|
||
if (it != players_.end() && it != players_.end() - 1) {
|
||
std::shared_ptr<Player> 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>& 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>& 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<int>(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);
|
||
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 |