diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index e80aa32..98e1a2f 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -393,15 +393,13 @@ void Player::setAlive(bool value) { if (!value) { death_sprite_->setPosX(head_sprite_->getRect().x); death_sprite_->setPosY(head_sprite_->getRect().y); - // MovingSprite comparteix vx/vy/ax/ay entre frame-based i time-based. - // De moment fixem els valors en px/frame perquè Game encara crida - // Player::update() frame-based. Quan Game flippi a time-based caldrà - // canviar a px/s (12.0 / -396.0 / +-198.0) — vegeu DEATH_*_PX_PER_S2/S. - death_sprite_->setAccelY(0.2F); - death_sprite_->setVelY(-6.6F); - death_sprite_->setVelX(3.3F); + // Física del cadàver en px/s i px/s² — Game crida Player::update(dt_s) + // que delega a death_sprite_->update(dt_s) (time-based). + death_sprite_->setAccelY(DEATH_ACCEL_Y_PX_PER_S2); + death_sprite_->setVelY(DEATH_VEL_Y_PX_PER_S); + death_sprite_->setVelX(DEATH_VEL_X_PX_PER_S); if (rand() % 2 == 0) { - death_sprite_->setVelX(-3.3F); + death_sprite_->setVelX(-DEATH_VEL_X_PX_PER_S); } } } diff --git a/source/game/game.cpp b/source/game/game.cpp index 10d2904..bce537e 100644 --- a/source/game/game.cpp +++ b/source/game/game.cpp @@ -20,13 +20,14 @@ #include "core/rendering/texture.h" // for Texture #include "core/resources/asset.h" // for Asset #include "core/resources/resource.h" -#include "game/defaults.hpp" // for PLAY_AREA_CENTER_X, BLOCK, PLAY_AREA_CEN... -#include "game/entities/balloon.h" // for Balloon, Balloon::VELX_NEGATIVE, BALLOON_... -#include "game/entities/bullet.h" // for Bullet, Bullet::Kind::LEFT, Bullet::Kind::RIGHT, BULLE... -#include "game/entities/item.h" // for Item -#include "game/entities/player.h" // for Player -#include "game/options.hpp" // for Options -#include "game/ui/menu.h" // for Menu +#include "core/system/delta_time.hpp" // for DeltaTime +#include "game/defaults.hpp" // for PLAY_AREA_CENTER_X, BLOCK, PLAY_AREA_CEN... +#include "game/entities/balloon.h" // for Balloon, Balloon::VELX_NEGATIVE, BALLOON_... +#include "game/entities/bullet.h" // for Bullet, Bullet::Kind::LEFT, Bullet::Kind::RIGHT, BULLE... +#include "game/entities/item.h" // for Item +#include "game/entities/player.h" // for Player +#include "game/options.hpp" // for Options +#include "game/ui/menu.h" // for Menu namespace Ja { struct Sound; } // namespace Ja @@ -102,6 +103,9 @@ Game::Game(int num_players, int current_stage, SDL_Renderer *renderer, bool demo // Inicializa las variables necesarias para la sección 'Game' init(); + + // Reset del rellotge perquè el primer dt_s no inclogui el temps de càrrega. + DeltaTime::reset(); } Game::~Game() { @@ -200,18 +204,27 @@ void Game::init() { game_completed_ = false; game_completed_counter_ = 0; + game_completed_counter_s_ = 0.0F; section_->name = SECTION_PROG_GAME; section_->subsection = SUBSECTION_GAME_PLAY_1P; menace_current_ = 0; menace_threshold_ = 0; hi_score_achieved_ = false; stage_bitmap_counter_ = STAGE_COUNTER; + stage_bitmap_counter_s_ = STAGE_COUNTER / 60.0F; death_counter_ = Player::DEATH_COUNTER; + death_counter_s_ = Player::DEATH_COUNTER / 60.0F; time_stopped_ = false; time_stopped_counter_ = 0; + time_stopped_counter_s_ = 0.0F; counter_ = 0; + elapsed_s_ = 0.0F; last_enemy_deploy_ = 0; enemy_deploy_counter_ = 0; + enemy_deploy_counter_s_ = 0.0F; + enemy_deploy_phase_s_ = 0.0F; + shake_phase_s_ = 0.0F; + helper_counter_s_ = 0.0F; enemy_speed_ = default_enemy_speed_; effect_.flash = false; effect_.shake = false; @@ -284,15 +297,15 @@ void Game::init() { // Con los globos creados, calcula el nivel de amenaza evaluateAndSetMenace(); - // Inicializa el bitmap de 1000 puntos + // Inicializa el bitmap de 1000 puntos (px/s i px/s²; -0.5 px/frame → -30 px/s) n1000_sprite_->setPosX(0); n1000_sprite_->setPosY(0); n1000_sprite_->setWidth(26); n1000_sprite_->setHeight(9); n1000_sprite_->setVelX(0.0F); - n1000_sprite_->setVelY(-0.5F); + n1000_sprite_->setVelY(-30.0F); n1000_sprite_->setAccelX(0.0F); - n1000_sprite_->setAccelY(-0.1F); + n1000_sprite_->setAccelY(-360.0F); n1000_sprite_->setSpriteClip(0, 0, 26, 9); n1000_sprite_->setEnabled(false); n1000_sprite_->setEnabledCounter(0); @@ -305,9 +318,9 @@ void Game::init() { n2500_sprite_->setWidth(28); n2500_sprite_->setHeight(9); n2500_sprite_->setVelX(0.0F); - n2500_sprite_->setVelY(-0.5F); + n2500_sprite_->setVelY(-30.0F); n2500_sprite_->setAccelX(0.0F); - n2500_sprite_->setAccelY(-0.1F); + n2500_sprite_->setAccelY(-360.0F); n2500_sprite_->setSpriteClip(26, 0, 28, 9); n2500_sprite_->setEnabled(false); n2500_sprite_->setEnabledCounter(0); @@ -320,9 +333,9 @@ void Game::init() { n5000_sprite_->setWidth(28); n5000_sprite_->setHeight(9); n5000_sprite_->setVelX(0.0F); - n5000_sprite_->setVelY(-0.5F); + n5000_sprite_->setVelY(-30.0F); n5000_sprite_->setAccelX(0.0F); - n5000_sprite_->setAccelY(-0.1F); + n5000_sprite_->setAccelY(-360.0F); n5000_sprite_->setSpriteClip(54, 0, 28, 9); n5000_sprite_->setEnabled(false); n5000_sprite_->setEnabledCounter(0); @@ -1450,6 +1463,26 @@ void Game::updatePlayers() { } } +// Actualiza las variables del jugador (time-based) +void Game::updatePlayers(float dt_s) { + for (auto *player : players_) { + player->update(dt_s); + + if (checkPlayerBalloonCollision(player)) { + if (player->isAlive()) { + if (demo_.enabled) { + section_->name = SECTION_PROG_TITLE; + section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS; + } else { + killPlayer(player); + } + } + } + + checkPlayerItemCollision(player); + } +} + // Dibuja a los jugadores void Game::renderPlayers() { for (auto *player : players_) { @@ -1497,6 +1530,45 @@ void Game::updateStage() { } } +// Actualiza las variables de la fase (time-based) +void Game::updateStage(float dt_s) { + if (stage_[current_stage_].current_power >= stage_[current_stage_].power_to_complete) { + current_stage_++; + last_stage_reached_ = current_stage_; + if (current_stage_ == 10) { + game_completed_ = true; + current_stage_ = 9; + stage_[current_stage_].current_power = 0; + destroyAllBalloons(); + stage_[current_stage_].current_power = 0; + menace_current_ = 255; + for (auto *player : players_) { + if (player->isAlive()) { + player->addScore(1000000); + } + } + updateHiScore(); + Audio::get()->stopMusic(); + } + Audio::get()->playSound(stage_change_sound_); + stage_bitmap_counter_ = 0; + stage_bitmap_counter_s_ = 0.0F; + enemy_speed_ = default_enemy_speed_; + setBalloonSpeed(enemy_speed_); + effect_.flash = true; + effect_.shake = true; + } + + if (stage_bitmap_counter_s_ < (STAGE_COUNTER / 60.0F)) { + stage_bitmap_counter_s_ += dt_s; + } + stage_bitmap_counter_ = std::min(STAGE_COUNTER, static_cast(stage_bitmap_counter_s_ * 60.0F)); + + if (game_completed_) { + stage_bitmap_counter_ = std::min(stage_bitmap_counter_, 100); + } +} + // Actualiza el estado de muerte void Game::updateDeath() { // Comprueba si todos los jugadores estan muertos @@ -1523,6 +1595,39 @@ void Game::updateDeath() { } } +// Actualiza el estado de muerte (time-based). Detecta el creuament dels llindars +// 250/200/180/120/60 (en frames) per a reproduir els bubbles als mateixos moments. +void Game::updateDeath(float dt_s) { + bool all_dead = true; + for (const auto *player : players_) { + all_dead &= (!player->isAlive()); + } + + if (!all_dead) { return; } + + if (death_counter_s_ <= 0.0F) { + section_->subsection = SUBSECTION_GAME_GAMEOVER; + return; + } + + const float PREV_S = death_counter_s_; + death_counter_s_ = std::max(0.0F, death_counter_s_ - dt_s); + death_counter_ = static_cast(death_counter_s_ * 60.0F); + + auto crossed = [&](float threshold_frames) { + const float TS = threshold_frames / 60.0F; + return (PREV_S > TS) && (death_counter_s_ <= TS); + }; + + if (crossed(250.0F) || crossed(200.0F) || crossed(180.0F) || crossed(120.0F) || crossed(60.0F)) { + if (!demo_.enabled) { + const Uint8 INDEX = rand() % 4; + Ja::Sound *sound[4] = {bubble1_sound_, bubble2_sound_, bubble3_sound_, bubble4_sound_}; + Audio::get()->playSound(sound[INDEX]); + } + } +} + // Renderiza el fade final cuando se acaba la partida void Game::renderDeathFade(int counter) { // Counter debe ir de 0 a 150 SDL_SetRenderDrawColor(renderer_, 0x27, 0x27, 0x36, 255); @@ -1555,6 +1660,13 @@ void Game::updateBalloons() { } } +// Actualiza los globos (time-based) +void Game::updateBalloons(float dt_s) { + for (auto *balloon : balloons_) { + balloon->update(dt_s); + } +} + // Pinta en pantalla todos los globos activos void Game::renderBalloons() { for (auto *balloon : balloons_) { @@ -1914,6 +2026,17 @@ void Game::moveBullets() { } } +// Mueve las balas activas (time-based) +void Game::moveBullets(float dt_s) { + for (auto *bullet : bullets_) { + if (bullet->isEnabled()) { + if (bullet->move(dt_s) == Bullet::MoveResult::OUT) { + players_[bullet->getOwner()]->decScoreMultiplier(); + } + } + } +} + // Pinta las balas activas void Game::renderBullets() { for (auto *bullet : bullets_) { @@ -1954,6 +2077,19 @@ void Game::updateItems() { } } +// Actualiza los items (time-based) +void Game::updateItems(float dt_s) { + for (auto *item : items_) { + if (item->isEnabled()) { + item->update(dt_s); + if (item->isOnFloor()) { + Audio::get()->playSound(coffee_machine_sound_); + effect_.shake = true; + } + } + } +} + // Pinta los items activos void Game::renderItems() { for (auto *item : items_) { @@ -2079,6 +2215,30 @@ void Game::updateShakeEffect() { } } +// Actualiza el efecto de agitar la pantalla (time-based). Decrementa +// `shake_counter` a cadència fixa de 60Hz amb un acumulador de fase +// (independent del framerate) — el render de `updateBackground` segueix +// llegint la paritat del counter per fer vibrar els edificis. +void Game::updateShakeEffect(float dt_s) { + if (!effect_.shake) { + shake_phase_s_ = 0.0F; + return; + } + constexpr float STEP_S = 1.0F / 60.0F; + shake_phase_s_ += dt_s; + while (shake_phase_s_ >= STEP_S) { + shake_phase_s_ -= STEP_S; + if (effect_.shake_counter > 0) { + effect_.shake_counter--; + } else { + effect_.shake = false; + effect_.shake_counter = SHAKE_COUNTER; + shake_phase_s_ = 0.0F; + break; + } + } +} + // Crea un SmartSprite para arrojar el item café al recibir un impacto void Game::throwCoffee(int x, int y) { auto *ss = new SmartSprite(item_textures_[4], renderer_); @@ -2088,10 +2248,12 @@ void Game::throwCoffee(int x, int y) { ss->setPosY(y - 8); ss->setWidth(16); ss->setHeight(16); - ss->setVelX(-1.0F + ((rand() % 5) * 0.5F)); - ss->setVelY(-4.0F); + // Conversió a px/s i px/s² (era -1..1 px/frame i 0.2 px/frame²) + const float VX_PX_PER_S = (-1.0F + (static_cast(rand() % 5) * 0.5F)) * 60.0F; + ss->setVelX(VX_PX_PER_S); + ss->setVelY(-240.0F); ss->setAccelX(0.0F); - ss->setAccelY(0.2F); + ss->setAccelY(720.0F); ss->setDestX(x + (ss->getVelX() * 50)); ss->setDestY(GAMECANVAS_HEIGHT + 1); ss->setEnabled(true); @@ -2109,6 +2271,13 @@ void Game::updateSmartSprites() { } } +// Actualiza los SmartSprites (time-based) +void Game::updateSmartSprites(float dt_s) { + for (auto *ss : smart_sprites_) { + ss->update(dt_s); + } +} + // Pinta los SmartSprites activos void Game::renderSmartSprites() { for (auto *ss : smart_sprites_) { @@ -2196,11 +2365,13 @@ auto Game::isTimeStopped() const -> bool { // Establece el valor de la variable void Game::setTimeStoppedCounter(Uint16 value) { time_stopped_counter_ = value; + time_stopped_counter_s_ = static_cast(value) / 60.0F; } // Incrementa el valor de la variable void Game::incTimeStoppedCounter(Uint16 value) { time_stopped_counter_ += value; + time_stopped_counter_s_ += static_cast(value) / 60.0F; } // Actualiza y comprueba el valor de la variable @@ -2215,6 +2386,18 @@ void Game::updateTimeStoppedCounter() { } } +// Actualiza y comprueba el valor de la variable (time-based) +void Game::updateTimeStoppedCounter(float dt_s) { + if (!isTimeStopped()) { return; } + if (time_stopped_counter_s_ > 0.0F) { + time_stopped_counter_s_ = std::max(0.0F, time_stopped_counter_s_ - dt_s); + time_stopped_counter_ = static_cast(time_stopped_counter_s_ * 60.0F); + stopAllBalloons(TIME_STOPPED_COUNTER); + } else { + disableTimeStopItem(); + } +} + // Actualiza la variable enemyDeployCounter void Game::updateEnemyDeployCounter() { if (enemy_deploy_counter_ > 0) { @@ -2222,6 +2405,19 @@ void Game::updateEnemyDeployCounter() { } } +// Actualiza enemy_deploy_counter_ (time-based). El comptador es decrementa +// a cadència fixa de 60Hz amb un acumulador de fase: és un comptador +// discret consultat per `canPowerBallBeCreated()` i altres. +void Game::updateEnemyDeployCounter(float dt_s) { + if (enemy_deploy_counter_ <= 0) { return; } + constexpr float STEP_S = 1.0F / 60.0F; + enemy_deploy_phase_s_ += dt_s; + while (enemy_deploy_phase_s_ >= STEP_S && enemy_deploy_counter_ > 0) { + enemy_deploy_phase_s_ -= STEP_S; + enemy_deploy_counter_--; + } +} + // Actualiza el juego void Game::update() { // Actualiza el audio @@ -2299,6 +2495,48 @@ void Game::update() { } } +// Actualiza el juego (time-based). Sense el gate del SDL_GetTicks: la cadència +// la dicta dt_s, propagat des de iterate() via DeltaTime::tick(). El comptador +// global `counter_` queda derivat de `elapsed_s_*60` perquè els lectors +// existents (render de l'herba, paths del get_ready, etc.) segueixin valuant. +void Game::update(float dt_s) { + Audio::update(); + + updateDeathShake(); + updateDeathSequence(); + + if (death_sequence_.phase == DeathPhase::SHAKING || death_sequence_.phase == DeathPhase::WAITING) { + return; + } + + // Acumulador i derivació del comptador legacy + elapsed_s_ += dt_s; + counter_ = static_cast(elapsed_s_ * 60.0F); + + checkGameInput(); + updatePlayers(dt_s); + updateBackground(dt_s); + updateBalloons(dt_s); + moveBullets(dt_s); + updateItems(dt_s); + updateStage(dt_s); + updateDeath(dt_s); + updateSmartSprites(dt_s); + updateTimeStoppedCounter(dt_s); + updateEnemyDeployCounter(dt_s); + updateShakeEffect(dt_s); + updateHelper(dt_s); + checkBulletBalloonCollision(); + updateMenace(); + updateBalloonSpeed(); + updateGameCompleted(dt_s); + + freeBullets(); + freeBalloons(); + freeItems(); + freeSmartSprites(); +} + // Actualiza el fondo void Game::updateBackground() { if (!game_completed_) { // Si el juego no esta completo, la velocidad de las nubes es igual a los globos explotados @@ -2314,13 +2552,13 @@ void Game::updateBackground() { // Calcula la velocidad en función de los globos explotados y el total de globos a explotar para acabar el juego const float SPEED = (-0.2F) + (-3.00F * ((float)clouds_speed_ / (float)total_power_to_complete_game_)); - // Aplica la velocidad calculada a las nubes + // Aplica la velocidad calculada a las nubes (px/frame) clouds1_a_->setVelX(SPEED); clouds1_b_->setVelX(SPEED); clouds2_a_->setVelX(SPEED / 2); clouds2_b_->setVelX(SPEED / 2); - // Mueve las nubes + // Mueve las nubes (frame-based) clouds1_a_->move(); clouds1_b_->move(); clouds2_a_->move(); @@ -2357,6 +2595,53 @@ void Game::updateBackground() { } } +// Actualiza el fondo (time-based). Velocitats dels núvols expressades com a +// px/frame (la conversió a px/s la fa cloud->move(dt_s) multiplicant per 60 +// internament — perquè MovingSprite comparteix vx_ entre frame i time-based, +// aquí passem la mateixa "velocitat per frame" * 60). +void Game::updateBackground(float dt_s) { + if (!game_completed_) { + clouds_speed_ = balloons_popped_; + } else { + if (clouds_speed_ > 400) { + clouds_speed_ -= 25; + } else { + clouds_speed_ = 200; + } + } + + // Velocitat per frame (mateixa fórmula); en time-based la passem com a px/s. + const float SPEED_PX_PER_FRAME = (-0.2F) + (-3.00F * ((float)clouds_speed_ / (float)total_power_to_complete_game_)); + const float SPEED_PX_PER_S = SPEED_PX_PER_FRAME * 60.0F; + + clouds1_a_->setVelX(SPEED_PX_PER_S); + clouds1_b_->setVelX(SPEED_PX_PER_S); + clouds2_a_->setVelX(SPEED_PX_PER_S / 2.0F); + clouds2_b_->setVelX(SPEED_PX_PER_S / 2.0F); + + clouds1_a_->move(dt_s); + clouds1_b_->move(dt_s); + clouds2_a_->move(dt_s); + clouds2_b_->move(dt_s); + + if (clouds1_a_->getPosX() < -clouds1_a_->getWidth()) { clouds1_a_->setPosX(clouds1_a_->getWidth()); } + if (clouds1_b_->getPosX() < -clouds1_b_->getWidth()) { clouds1_b_->setPosX(clouds1_b_->getWidth()); } + if (clouds2_a_->getPosX() < -clouds2_a_->getWidth()) { clouds2_a_->setPosX(clouds2_a_->getWidth()); } + if (clouds2_b_->getPosX() < -clouds2_b_->getWidth()) { clouds2_b_->setPosX(clouds2_b_->getWidth()); } + + // Herba: `counter_` derivat de `elapsed_s_*60` ja oscil·la a 60Hz. + grass_sprite_->setSpriteClip(0, (6 * (counter_ / 20 % 2)), 256, 6); + + if (death_shake_.active) { + const int V[] = {-1, 1, -1, 1, -1, 1, -1, 0}; + buildings_sprite_->setPosX(V[death_shake_.step]); + } else if (effect_.shake) { + buildings_sprite_->setPosX(((effect_.shake_counter % 2) * 2) - 1); + } else { + buildings_sprite_->setPosX(0); + } +} + // Dibuja el fondo void Game::renderBackground() { const float GRADIENT_NUMBER = std::min(((float)balloons_popped_ / 1250.0F), 3.0F); @@ -2679,55 +2964,62 @@ auto Game::isDeathShaking() const -> bool { // Ejecuta un frame del juego void Game::iterate() { - // En modo demo, no hay pausa ni game over - if (demo_.enabled) { - if (section_->subsection == SUBSECTION_GAME_PAUSE || section_->subsection == SUBSECTION_GAME_GAMEOVER) { - section_->name = SECTION_PROG_TITLE; - section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS; - return; - } + // Consum del temps real des de l'última iteració. Sempre s'ha de cridar + // perquè el rellotge no acumuli a través de transicions/sub-estats. + const float DELTA_TIME_S = DeltaTime::tick(); + + // En modo demo, ni pause ni game over: torna immediatament al títol + if (demo_.enabled && (section_->subsection == SUBSECTION_GAME_PAUSE || section_->subsection == SUBSECTION_GAME_GAMEOVER)) { + section_->name = SECTION_PROG_TITLE; + section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS; + return; } - // Sección juego en pausa - if (section_->subsection == SUBSECTION_GAME_PAUSE) { - if (!pause_initialized_) { - enterPausedGame(); - } - updatePausedGame(); - renderPausedGame(); + switch (section_->subsection) { + case SUBSECTION_GAME_PAUSE: + iteratePaused(); + break; + case SUBSECTION_GAME_GAMEOVER: + iterateGameOver(); + break; + case SUBSECTION_GAME_PLAY_1P: + case SUBSECTION_GAME_PLAY_2P: + iteratePlaying(DELTA_TIME_S); + break; + default: + break; + } +} + +// Rama de iterate(): pause +void Game::iteratePaused() { + if (!pause_initialized_) { enterPausedGame(); } + updatePausedGame(); + renderPausedGame(); +} + +// Rama de iterate(): game over +void Game::iterateGameOver() { + if (!game_over_initialized_) { enterGameOverScreen(); } + updateGameOverScreen(); + renderGameOverScreen(); +} + +// Rama de iterate(): joc actiu +void Game::iteratePlaying(float dt_s) { + // Si veníem de Pause/GameOver, el dt acumulat seria enorme; descarta'l. + if (pause_initialized_ || game_over_initialized_) { + DeltaTime::reset(); + } + pause_initialized_ = false; + game_over_initialized_ = false; + + if (Audio::getRealMusicState() == Audio::MusicState::STOPPED && !game_completed_ && !demo_.enabled && players_[0]->isAlive()) { + Audio::get()->playMusic(game_music_); } - // Sección Game Over - else if (section_->subsection == SUBSECTION_GAME_GAMEOVER) { - if (!game_over_initialized_) { - enterGameOverScreen(); - } - updateGameOverScreen(); - renderGameOverScreen(); - } - - // Sección juego jugando - else if ((section_->subsection == SUBSECTION_GAME_PLAY_1P) || (section_->subsection == SUBSECTION_GAME_PLAY_2P)) { - // Resetea los flags de inicialización de sub-estados - pause_initialized_ = false; - game_over_initialized_ = false; - - // Si la música no está sonando - if ((Audio::getRealMusicState() == Audio::MusicState::STOPPED) || (Audio::getRealMusicState() == Audio::MusicState::STOPPED)) { - // Reproduce la música (nunca en modo demo: deja sonar la del título) - if (!game_completed_ && !demo_.enabled) { - if (players_[0]->isAlive()) { - Audio::get()->playMusic(game_music_); - } - } - } - - // Actualiza la lógica del juego - update(); - - // Dibuja los objetos - render(); - } + update(dt_s); + render(); } // Indica si el juego ha terminado @@ -3109,6 +3401,19 @@ void Game::updateGameCompleted() { } } +// Actualiza el tramo final de juego (time-based) +void Game::updateGameCompleted(float dt_s) { + if (!game_completed_) { return; } + + const int PREV = game_completed_counter_; + game_completed_counter_s_ += dt_s; + game_completed_counter_ = static_cast(game_completed_counter_s_ * 60.0F); + + if (PREV < GAME_COMPLETED_END && game_completed_counter_ >= GAME_COMPLETED_END) { + section_->subsection = SUBSECTION_GAME_GAMEOVER; + } +} + // Actualiza las variables de ayuda void Game::updateHelper() { // Solo ofrece ayuda cuando la amenaza es elevada @@ -3124,6 +3429,20 @@ void Game::updateHelper() { } } +// Actualiza las variables de ayuda (time-based). De moment cap timer real +// dins helper_; clonem la lògica per consistència d'API. +void Game::updateHelper([[maybe_unused]] float dt_s) { + if (menace_current_ > 15) { + for (const auto *player : players_) { + helper_.need_coffee = player->getCoffees() == 0; + helper_.need_coffee_machine = !player->isPowerUp(); + } + } else { + helper_.need_coffee = false; + helper_.need_coffee_machine = false; + } +} + // Comprueba si todos los jugadores han muerto auto Game::allPlayersAreDead() -> bool { bool success = true; diff --git a/source/game/game.h b/source/game/game.h index a593ffc..30e0fbb 100644 --- a/source/game/game.h +++ b/source/game/game.h @@ -38,6 +38,11 @@ class Game { void handleEvent(const SDL_Event *event); // Procesa un evento private: + // Branques de iterate() — separades per a reduir la complexitat cognitiva + void iteratePaused(); + void iterateGameOver(); + void iteratePlaying(float dt_s); + // Cantidad de elementos a escribir en los ficheros de datos static constexpr int TOTAL_SCORE_DATA = 3; static constexpr int TOTAL_DEMO_DATA = 2000; @@ -141,10 +146,11 @@ class Game { DemoKeys data_file[TOTAL_DEMO_DATA]; // Datos del fichero con los movimientos para la demo }; - void update(); // Actualiza el juego - void render(); // Dibuja el juego - void init(); // Inicializa las variables necesarias para la sección 'Game' - void loadMedia(); // Carga los recursos necesarios para la sección 'Game' + void update(); // Actualiza el juego (frame-based) + void update(float dt_s); // Actualiza el juego (time-based) + void render(); // Dibuja el juego + void init(); // Inicializa las variables necesarias para la sección 'Game' + void loadMedia(); // Carga los recursos necesarios para la sección 'Game' auto loadScoreFile() -> bool; // Carga el fichero de puntos auto loadDemoFile() -> bool; // Carga el fichero de datos para la demo @@ -167,14 +173,18 @@ class Game { static auto updateScoreText(Uint32 num) -> std::string; // Transforma un valor numérico en una cadena de 6 cifras void renderScoreBoard(); // Pinta el marcador en pantalla usando un objeto texto - void updatePlayers(); // Actualiza las variables del jugador - void renderPlayers(); // Dibuja a los jugadores + void updatePlayers(); // Actualiza las variables del jugador (frame-based) + void updatePlayers(float dt_s); // Actualiza las variables del jugador (time-based) + void renderPlayers(); // Dibuja a los jugadores - void updateStage(); // Actualiza las variables de la fase - void updateDeath(); // Actualiza el estado de muerte + void updateStage(); // Actualiza las variables de la fase (frame-based) + void updateStage(float dt_s); // Actualiza las variables de la fase (time-based) + void updateDeath(); // Actualiza el estado de muerte (frame-based) + void updateDeath(float dt_s); // Actualiza el estado de muerte (time-based) void renderDeathFade(int counter); // Renderiza el fade final cuando se acaba la partida - void updateBalloons(); // Actualiza los globos + void updateBalloons(); // Actualiza los globos (frame-based) + void updateBalloons(float dt_s); // Actualiza los globos (time-based) void renderBalloons(); // Pinta en pantalla todos los globos activos auto createBalloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 creationtimer) -> Uint8; // Crea un globo nuevo en el vector de globos void createPowerBall(); // Crea una PowerBall @@ -193,12 +203,14 @@ class Game { void checkBulletBalloonCollision(); // Comprueba la colisión entre las balas y los globos void resolveBulletBalloonHit(Bullet *bullet, Balloon *balloon); // Resuelve un impacto bala-globo (helper de checkBulletBalloonCollision) - void moveBullets(); // Mueve las balas activas + void moveBullets(); // Mueve las balas activas (frame-based) + void moveBullets(float dt_s); // Mueve las balas activas (time-based) void renderBullets(); // Pinta las balas activas void createBullet(int x, int y, Bullet::Kind kind, bool powered_up, int owner); // Crea un objeto bala void freeBullets(); // Vacia el vector de balas - void updateItems(); // Actualiza los items + void updateItems(); // Actualiza los items (frame-based) + void updateItems(float dt_s); // Actualiza los items (time-based) void renderItems(); // Pinta los items activos auto dropItem() -> Item::Id; // Devuelve un item en función del azar void createItem(Item::Id kind, float x, float y); // Crea un objeto item @@ -207,11 +219,13 @@ class Game { void createItemScoreSprite(int x, int y, const SmartSprite *sprite); // Crea un objeto SmartSprite void freeSmartSprites(); // Vacia el vector de smartsprites - void renderFlashEffect(); // Dibuja el efecto de flash - void updateShakeEffect(); // Actualiza el efecto de agitar la pantalla - void throwCoffee(int x, int y); // Crea un SmartSprite para arrojar el item café al recibir un impacto - void updateSmartSprites(); // Actualiza los SmartSprites - void renderSmartSprites(); // Pinta los SmartSprites activos + void renderFlashEffect(); // Dibuja el efecto de flash + void updateShakeEffect(); // Actualiza el efecto de agitar la pantalla (frame-based) + void updateShakeEffect(float dt_s); // Actualiza el efecto de agitar la pantalla (time-based) + void throwCoffee(int x, int y); // Crea un SmartSprite para arrojar el item café al recibir un impacto + void updateSmartSprites(); // Actualiza los SmartSprites (frame-based) + void updateSmartSprites(float dt_s); // Actualiza los SmartSprites (time-based) + void renderSmartSprites(); // Pinta los SmartSprites activos void killPlayer(Player *player); // Acciones a realizar cuando el jugador muere void evaluateAndSetMenace(); // Calcula y establece el valor de amenaza en funcion de los globos activos @@ -222,11 +236,14 @@ class Game { void setTimeStoppedCounter(Uint16 value); // Establece el valor de la variable void incTimeStoppedCounter(Uint16 value); // Incrementa el valor de la variable - void updateEnemyDeployCounter(); // Actualiza la variable EnemyDeployCounter - void updateTimeStoppedCounter(); // Actualiza y comprueba el valor de la variable - void updateMenace(); // Gestiona el nivel de amenaza - void updateBackground(); // Actualiza el fondo - void renderBackground(); // Dibuja el fondo + void updateEnemyDeployCounter(); // Actualiza la variable EnemyDeployCounter (frame-based) + void updateEnemyDeployCounter(float dt_s); // Actualiza la variable EnemyDeployCounter (time-based) + void updateTimeStoppedCounter(); // Actualiza y comprueba el valor de la variable (frame-based) + void updateTimeStoppedCounter(float dt_s); // Actualiza y comprueba el valor de la variable (time-based) + void updateMenace(); // Gestiona el nivel de amenaza + void updateBackground(); // Actualiza el fondo (frame-based) + void updateBackground(float dt_s); // Actualiza el fondo (time-based) + void renderBackground(); // Dibuja el fondo void checkGameInput(); // Gestiona la entrada durante el juego void processDemoInput(); // Helper de checkGameInput @@ -254,8 +271,10 @@ class Game { auto canPowerBallBeCreated() -> bool; // Indica si se puede crear una powerball auto calculateScreenPower() -> int; // Calcula el poder actual de los globos en pantalla void initPaths(); // Inicializa las variables que contienen puntos de ruta para mover objetos - void updateGameCompleted(); // Actualiza el tramo final de juego, una vez completado - void updateHelper(); // Actualiza las variables de ayuda + void updateGameCompleted(); // Actualiza el tramo final de juego (frame-based) + void updateGameCompleted(float dt_s); // Actualiza el tramo final de juego (time-based) + void updateHelper(); // Actualiza las variables de ayuda (frame-based) + void updateHelper(float dt_s); // Actualiza las variables de ayuda (time-based) auto allPlayersAreDead() -> bool; // Comprueba si todos los jugadores han muerto void deleteAllVectorObjects(); // Elimina todos los objetos contenidos en vectores void setHiScore(); // Establece la máxima puntuación desde fichero o desde las puntuaciones online @@ -337,30 +356,38 @@ class Game { // Variables int num_players_; // Numero de jugadores - Uint32 ticks_; // Contador de ticks para ajustar la velocidad del programa - Uint8 ticks_speed_; // Velocidad a la que se repiten los bucles del programa + Uint32 ticks_; // Contador de ticks para ajustar la velocidad del programa (frame-based) + Uint8 ticks_speed_; // Velocidad a la que se repiten los bucles del programa (frame-based) + float elapsed_s_{0.0F}; // Acumulador global de temps de joc (time-based) Uint32 hi_score_; // Puntuación máxima bool hi_score_achieved_; // Indica si se ha superado la puntuación máxima std::string hi_score_name_; // Nombre del jugador que ostenta la máxima puntuación Stage stage_[10]; // Variable con los datos de cada pantalla Uint8 current_stage_; // Indica la fase actual - Uint8 stage_bitmap_counter_; // Contador para el tiempo visible del texto de Stage + Uint8 stage_bitmap_counter_; // Contador para el tiempo visible del texto de Stage (frame-based) + float stage_bitmap_counter_s_{0.0F}; // Contador (time-based) float stage_bitmap_path_[STAGE_COUNTER]; // Vector con los puntos Y por donde se desplaza el texto float get_ready_bitmap_path_[STAGE_COUNTER]; // Vector con los puntos X por donde se desplaza el texto - Uint16 death_counter_; // Contador para la animación de muerte del jugador + Uint16 death_counter_; // Contador para la animación de muerte del jugador (frame-based) + float death_counter_s_{0.0F}; // Contador (time-based) Uint8 menace_current_; // Nivel de amenaza actual Uint8 menace_threshold_; // Umbral del nivel de amenaza. Si el nivel de amenaza cae por debajo del umbral, se generan más globos. Si el umbral aumenta, aumenta el numero de globos bool time_stopped_; // Indica si el tiempo está detenido - Uint16 time_stopped_counter_; // Temporizador para llevar la cuenta del tiempo detenido - Uint32 counter_; // Contador para el juego + Uint16 time_stopped_counter_; // Temporizador (frame-based) + float time_stopped_counter_s_{0.0F}; // Temporizador (time-based) + Uint32 counter_; // Contador para el juego (frame-based, derivat de elapsed_s_*60 en time-based) Uint32 score_data_file_[TOTAL_SCORE_DATA]; // Datos del fichero de puntos SDL_Rect sky_colors_rect_[4]; // Vector con las coordenadas de los 4 colores de cielo Uint16 balloons_popped_; // Lleva la cuenta de los globos explotados Uint8 last_enemy_deploy_; // Guarda cual ha sido la última formación desplegada para no repetir; - int enemy_deploy_counter_; // Cuando se lanza una formación, se le da un valor y no sale otra hasta que llegue a cero + int enemy_deploy_counter_; // Cuando se lanza una formación, se le da un valor y no sale otra hasta que llegue a cero (frame-based) + float enemy_deploy_counter_s_{0.0F}; // Comptador (time-based) float enemy_speed_; // Velocidad a la que se mueven los enemigos float default_enemy_speed_; // Velocidad base de los enemigos, sin incrementar Effect effect_; // Variable para gestionar los efectos visuales + float shake_phase_s_{0.0F}; // Acumulador per decrementar shake_counter a 60Hz (time-based) + float helper_counter_s_{0.0F}; // Acumulador per al comptador helper_.counter (time-based) + float enemy_deploy_phase_s_{0.0F}; // Acumulador per al decrement de enemy_deploy_counter_ (time-based) DeathShake death_shake_; // Variable para gestionar el efecto de agitación intensa DeathSequence death_sequence_; // Variable para gestionar la secuencia de muerte Helper helper_; // Variable para gestionar las ayudas @@ -368,7 +395,8 @@ class Game { Uint8 power_ball_counter_; // Contador de formaciones enemigas entre la aparicion de una PowerBall y otra bool coffee_machine_enabled_; // Indica si hay una máquina de café en el terreno de juego bool game_completed_; // Indica si se ha completado la partida, llegando al final de la ultima pantalla - int game_completed_counter_; // Contador para el tramo final, cuando se ha completado la partida y ya no aparecen más enemigos + int game_completed_counter_; // Contador per al tram final (frame-based) + float game_completed_counter_s_{0.0F}; // Comptador (time-based) Uint8 difficulty_; // Dificultad del juego float difficulty_score_multiplier_; // Multiplicador de puntos en función de la dificultad Color difficulty_color_; // Color asociado a la dificultad