#include "player.hpp" #include // Para SDL_FlipMode #include // Para clamp, max, min #include // Para fmod #include // Para rand #include "animated_sprite.hpp" // Para AnimatedSprite #include "asset.hpp" // Para Asset #include "audio.hpp" // Para Audio #include "cooldown.hpp" // Para Cooldown #include "input.hpp" // Para Input #include "input_types.hpp" // Para InputAction #include "manage_hiscore_table.hpp" // Para ManageHiScoreTable, HiScoreEntry #include "param.hpp" // Para Param, ParamGame, param #include "scoreboard.hpp" // Para Scoreboard #include "stage_interface.hpp" // Para IStageInfo #include "texture.hpp" // Para Texture // Constructor Player::Player(const Config& config) : player_sprite_(std::make_unique(config.texture.at(0), config.animations.at(0))), power_sprite_(std::make_unique(config.texture.at(4), config.animations.at(1))), enter_name_(std::make_unique()), cooldown_(std::make_unique(0.300F, 0.140F)), hi_score_table_(config.hi_score_table), glowing_entry_(config.glowing_entry), stage_info_(config.stage_info), play_area_(*config.play_area), id_(config.id), default_pos_x_(config.x), default_pos_y_(config.y), demo_(config.demo) { // Configura objetos player_sprite_->addTexture(config.texture.at(1)); player_sprite_->addTexture(config.texture.at(2)); player_sprite_->addTexture(config.texture.at(3)); player_sprite_->setActiveTexture(coffees_); power_sprite_->getTexture()->setAlpha(224); power_up_x_offset_ = (power_sprite_->getWidth() - player_sprite_->getWidth()) / 2; power_sprite_->setPosY(default_pos_y_ - (power_sprite_->getHeight() - player_sprite_->getHeight())); // Inicializa variables pos_x_ = default_pos_x_; init(); } // Iniciador void Player::init() { // Inicializa variables de estado pos_y_ = default_pos_y_; walking_state_ = State::WALKING_STOP; firing_state_ = State::FIRING_NONE; invulnerable_ = false; invulnerable_counter_ = 0; power_up_ = false; power_up_counter_ = POWERUP_COUNTER; extra_hit_ = false; coffees_ = 0; continue_counter_ = 10; name_entry_idle_time_accumulator_ = 0.0F; name_entry_total_time_accumulator_ = 0.0F; shiftColliders(); vel_x_ = 0; vel_y_ = 0; score_ = 0; score_multiplier_ = 1.0F; enter_name_->init(last_enter_name_); // Establece la posición del sprite player_sprite_->clear(); shiftSprite(); } // Actua en consecuencia de la entrada recibida void Player::setInput(Input::Action action) { switch (playing_state_) { case State::PLAYING: { setInputPlaying(action); break; } case State::SHOWING_NAME: case State::ENTERING_NAME: case State::ENTERING_NAME_GAME_COMPLETED: { setInputEnteringName(action); break; } default: break; } } // Procesa inputs para cuando está jugando void Player::setInputPlaying(Input::Action action) { switch (action) { case Input::Action::LEFT: { vel_x_ = -BASE_SPEED; setWalkingState(State::WALKING_LEFT); break; } case Input::Action::RIGHT: { vel_x_ = BASE_SPEED; setWalkingState(State::WALKING_RIGHT); break; } case Input::Action::FIRE_CENTER: { setFiringState(State::FIRING_UP); break; } case Input::Action::FIRE_LEFT: { setFiringState(State::FIRING_LEFT); break; } case Input::Action::FIRE_RIGHT: { setFiringState(State::FIRING_RIGHT); break; } default: { vel_x_ = 0; setWalkingState(State::WALKING_STOP); break; } } } // Procesa inputs para cuando está introduciendo el nombre void Player::setInputEnteringName(Input::Action action) { switch (action) { case Input::Action::FIRE_LEFT: // Añade una letra if (isShowingName()) { passShowingName(); } else { if (enter_name_->endCharSelected()) { last_enter_name_ = getRecordName(); setPlayingState(Player::State::SHOWING_NAME); playSound("name_input_accept.wav"); } else { enter_name_->addCharacter(); playSound("service_menu_select.wav"); } } break; case Input::Action::FIRE_CENTER: // Borra una letra if (isShowingName()) { passShowingName(); } else { if (!enter_name_->nameIsEmpty()) { enter_name_->removeLastCharacter(); playSound("service_menu_back.wav"); } } break; case Input::Action::RIGHT: if (!isShowingName() && !enter_name_->nameIsFull()) { if (cooldown_->tryConsumeOnHeld()) { enter_name_->incIndex(); playSound("service_menu_move.wav"); } } break; case Input::Action::LEFT: if (!isShowingName() && !enter_name_->nameIsFull()) { if (cooldown_->tryConsumeOnHeld()) { enter_name_->decIndex(); playSound("service_menu_move.wav"); } } break; case Input::Action::START: if (isShowingName()) { passShowingName(); } else { last_enter_name_ = getRecordName(); setPlayingState(Player::State::SHOWING_NAME); playSound("name_input_accept.wav"); } break; default: cooldown_->onReleased(); break; } name_entry_idle_time_accumulator_ = 0.0F; } // Sistema de movimiento void Player::move(float delta_time) { switch (playing_state_) { case State::PLAYING: handlePlayingMovement(delta_time); break; case State::ROLLING: handleRollingMovement(); break; case State::TITLE_ANIMATION: handleTitleAnimation(delta_time); break; case State::CONTINUE_TIME_OUT: handleContinueTimeOut(); break; case State::LEAVING_SCREEN: updateStepCounter(delta_time); handleLeavingScreen(delta_time); break; case State::ENTERING_SCREEN: updateStepCounter(delta_time); handleEnteringScreen(delta_time); break; case State::CREDITS: handleCreditsMovement(delta_time); break; case State::WAITING: handleWaitingMovement(delta_time); break; case State::RECOVER: handleRecoverMovement(); break; default: break; } } // Movimiento time-based durante el juego void Player::handlePlayingMovement(float delta_time) { // Mueve el jugador a derecha o izquierda (time-based en segundos) pos_x_ += vel_x_ * delta_time; // Si el jugador abandona el area de juego por los laterales, restaura su posición const float MIN_X = play_area_.x - 5; const float MAX_X = play_area_.w + 5 - WIDTH; pos_x_ = std::clamp(pos_x_, MIN_X, MAX_X); shiftSprite(); } void Player::handleRecoverMovement() { if (player_sprite_->getCurrentAnimationFrame() == 10 && !recover_sound_triggered_) { playSound("voice_recover.wav"); recover_sound_triggered_ = true; } if (player_sprite_->animationIsCompleted()) { setPlayingState(State::RESPAWNING); } } void Player::handleRollingMovement() { handleRollingBoundaryCollision(); handleRollingGroundCollision(); } void Player::handleRollingBoundaryCollision() { const int X = player_sprite_->getPosX(); const int MIN_X = play_area_.x; const int MAX_X = play_area_.x + play_area_.w - WIDTH; if ((X < MIN_X) || (X > MAX_X)) { player_sprite_->setPosX(std::clamp(X, MIN_X, MAX_X)); player_sprite_->setVelX(-player_sprite_->getVelX()); playSound("jump.wav"); } } void Player::handleRollingGroundCollision() { if (player_sprite_->getPosY() <= play_area_.h - HEIGHT) { return; } if (player_sprite_->getVelY() < 120.0F) { // 2.0F * 60fps = 120.0F pixels/segundo handleRollingStop(); } else { handleRollingBounce(); } } void Player::handleRollingStop() { const auto NEXT_PLAYER_STATE = qualifiesForHighScore() ? State::ENTERING_NAME : State::CONTINUE; const auto NEXT_STATE = demo_ ? State::LYING_ON_THE_FLOOR_FOREVER : NEXT_PLAYER_STATE; setPlayingState(NEXT_STATE); pos_x_ = player_sprite_->getPosX(); pos_y_ = default_pos_y_; player_sprite_->clear(); shiftSprite(); playSound("jump.wav"); } void Player::handleRollingBounce() { player_sprite_->setPosY(play_area_.h - HEIGHT); player_sprite_->setVelY(player_sprite_->getVelY() * -0.5F); player_sprite_->setVelX(player_sprite_->getVelX() * 0.75F); player_sprite_->setAnimationSpeed(player_sprite_->getAnimationSpeed() * 2); playSound("jump.wav"); } void Player::handleTitleAnimation(float delta_time) { setInputBasedOnPlayerId(); pos_x_ += (vel_x_ * 2.0F) * delta_time; const float MIN_X = -WIDTH; const float MAX_X = play_area_.w; pos_x_ = std::clamp(pos_x_, MIN_X, MAX_X); shiftSprite(); if (pos_x_ == MIN_X || pos_x_ == MAX_X) { setPlayingState(State::TITLE_HIDDEN); } } void Player::handleContinueTimeOut() { // Si el cadaver desaparece por el suelo, cambia de estado if (player_sprite_->getPosY() > play_area_.h) { player_sprite_->stop(); setPlayingState(State::WAITING); } } void Player::handleLeavingScreen(float delta_time) { // updateStepCounter se llama desde move() con deltaTime setInputBasedOnPlayerId(); pos_x_ += vel_x_ * delta_time; const float MIN_X = -WIDTH; const float MAX_X = play_area_.w; pos_x_ = std::clamp(pos_x_, MIN_X, MAX_X); shiftSprite(); if (pos_x_ == MIN_X || pos_x_ == MAX_X) { setPlayingState(State::GAME_OVER); } } void Player::handleEnteringScreen(float delta_time) { // updateStepCounter se llama desde move() con deltaTime switch (id_) { case Id::PLAYER1: handlePlayer1Entering(delta_time); break; case Id::PLAYER2: handlePlayer2Entering(delta_time); break; default: break; } shiftSprite(); } void Player::handlePlayer1Entering(float delta_time) { setInputPlaying(Input::Action::RIGHT); pos_x_ += vel_x_ * delta_time; if (pos_x_ > default_pos_x_) { pos_x_ = default_pos_x_; setPlayingState(State::PLAYING); } } void Player::handlePlayer2Entering(float delta_time) { setInputPlaying(Input::Action::LEFT); pos_x_ += vel_x_ * delta_time; if (pos_x_ < default_pos_x_) { pos_x_ = default_pos_x_; setPlayingState(State::PLAYING); } } // Movimiento general en la pantalla de créditos (time-based) void Player::handleCreditsMovement(float delta_time) { pos_x_ += (vel_x_ / 2.0F) * delta_time; if (vel_x_ > 0) { handleCreditsRightMovement(); } else { handleCreditsLeftMovement(); } updateWalkingStateForCredits(); shiftSprite(); } void Player::handleCreditsRightMovement() { if (pos_x_ > param.game.game_area.rect.w - WIDTH) { pos_x_ = param.game.game_area.rect.w - WIDTH; vel_x_ *= -1; } } void Player::handleCreditsLeftMovement() { if (pos_x_ < param.game.game_area.rect.x) { pos_x_ = param.game.game_area.rect.x; vel_x_ *= -1; } } // Controla la animación del jugador saludando (time-based) void Player::handleWaitingMovement(float delta_time) { waiting_time_accumulator_ += delta_time; const float WAITING_DURATION_S = static_cast(WAITING_COUNTER) / 60.0F; // Convert frames to seconds if (waiting_time_accumulator_ >= WAITING_DURATION_S) { waiting_time_accumulator_ = 0.0F; player_sprite_->resetAnimation(); } } void Player::updateWalkingStateForCredits() { if (pos_x_ > param.game.game_area.center_x - WIDTH / 2) { setWalkingState(State::WALKING_LEFT); } else { setWalkingState(State::WALKING_RIGHT); } } void Player::setInputBasedOnPlayerId() { switch (id_) { case Id::PLAYER1: setInputPlaying(Input::Action::LEFT); break; case Id::PLAYER2: setInputPlaying(Input::Action::RIGHT); break; default: break; } } // Incrementa o ajusta el contador de pasos (time-based) void Player::updateStepCounter(float delta_time) { step_time_accumulator_ += delta_time; const float STEP_INTERVAL_S = 10.0F / 60.0F; // 10 frames converted to seconds if (step_time_accumulator_ >= STEP_INTERVAL_S) { step_time_accumulator_ = 0.0F; playSound("walk.wav"); } } // Pinta el jugador en pantalla void Player::render() { if (power_sprite_visible_ && isPlaying()) { power_sprite_->render(); } if (isRenderable()) { player_sprite_->render(); } } // Calcula la animacion de moverse y disparar del jugador auto Player::computeAnimation() const -> std::pair { const std::string BASE_ANIM = (walking_state_ == State::WALKING_STOP) ? "stand" : "walk"; std::string anim_name; SDL_FlipMode flip_mode = SDL_FLIP_NONE; switch (firing_state_) { case State::FIRING_NONE: anim_name = BASE_ANIM; flip_mode = (walking_state_ == State::WALKING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; break; case State::FIRING_UP: anim_name = BASE_ANIM + "-fire-center"; flip_mode = (walking_state_ == State::WALKING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; break; case State::FIRING_LEFT: case State::FIRING_RIGHT: anim_name = BASE_ANIM + "-fire-side"; flip_mode = (firing_state_ == State::FIRING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; break; case State::RECOILING_UP: anim_name = BASE_ANIM + "-recoil-center"; flip_mode = (walking_state_ == State::WALKING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; break; case State::RECOILING_LEFT: case State::RECOILING_RIGHT: anim_name = BASE_ANIM + "-recoil-side"; flip_mode = (firing_state_ == State::RECOILING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; break; case State::COOLING_UP: anim_name = BASE_ANIM + "-cool-center"; flip_mode = (walking_state_ == State::WALKING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; break; case State::COOLING_LEFT: case State::COOLING_RIGHT: anim_name = BASE_ANIM + "-cool-side"; flip_mode = (firing_state_ == State::COOLING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; break; default: anim_name = BASE_ANIM; flip_mode = (walking_state_ == State::WALKING_RIGHT) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; break; } return {anim_name, flip_mode}; } // Establece la animación correspondiente al estado void Player::setAnimation(float delta_time) { switch (playing_state_) { case State::PLAYING: case State::ENTERING_SCREEN: case State::LEAVING_SCREEN: case State::TITLE_ANIMATION: case State::CREDITS: { auto [animName, flipMode] = computeAnimation(); player_sprite_->setCurrentAnimation(animName, false); player_sprite_->setFlip(flipMode); break; } case State::RECOVER: player_sprite_->setCurrentAnimation("recover"); break; case State::WAITING: case State::GAME_OVER: player_sprite_->setCurrentAnimation("hello"); break; case State::ROLLING: case State::CONTINUE_TIME_OUT: player_sprite_->setCurrentAnimation("rolling"); break; case State::LYING_ON_THE_FLOOR_FOREVER: case State::ENTERING_NAME: case State::CONTINUE: player_sprite_->setCurrentAnimation("dizzy"); break; case State::ENTERING_NAME_GAME_COMPLETED: case State::CELEBRATING: player_sprite_->setCurrentAnimation("celebration"); break; default: break; } // La diferencia clave: usa deltaTime para las animaciones player_sprite_->update(delta_time); power_sprite_->update(delta_time); } // Actualiza al jugador con deltaTime (time-based) void Player::update(float delta_time) { move(delta_time); // Sistema de movimiento setAnimation(delta_time); // Animaciones shiftColliders(); // Actualiza caja de colisiones updateFireSystem(delta_time); // Sistema de disparo de dos líneas updatePowerUp(delta_time); // Sistema de power-up updateInvulnerable(delta_time); // Sistema de invulnerabilidad updateScoreboard(); // Sistema de marcador updateContinueCounter(delta_time); // Sistema de continue updateEnterNameCounter(delta_time); // Sistema de name entry updateShowingName(delta_time); // Sistema de showing name cooldown_->update(delta_time); } void Player::passShowingName() { if (game_completed_) { setPlayingState(State::LEAVING_SCREEN); } else { setPlayingState(State::CONTINUE); } } // Incrementa la puntuación del jugador void Player::addScore(int score, int lowest_hi_score_entry) { if (isPlaying()) { score_ += score; qualifies_for_high_score_ = score_ > lowest_hi_score_entry; } } // Actualiza el panel del marcador void Player::updateScoreboard() { switch (playing_state_) { case State::CONTINUE: { Scoreboard::get()->setContinue(scoreboard_panel_, getContinueCounter()); break; } case State::ENTERING_NAME: case State::ENTERING_NAME_GAME_COMPLETED: { Scoreboard::get()->setEnterName(scoreboard_panel_, enter_name_->getCurrentName()); Scoreboard::get()->setCharacterSelected(scoreboard_panel_, enter_name_->getSelectedCharacter()); Scoreboard::get()->setCarouselAnimation(scoreboard_panel_, enter_name_->getSelectedIndex(), enter_name_.get()); break; } default: break; } } // Cambia el modo del marcador void Player::setScoreboardMode(Scoreboard::Mode mode) const { if (!demo_) { Scoreboard::get()->setMode(scoreboard_panel_, mode); } } // Establece el estado del jugador en el juego void Player::setPlayingState(State state) { playing_state_ = state; switch (playing_state_) { case State::RECOVER: { recover_sound_triggered_ = false; score_ = 0; // Pon los puntos a cero para que no se vea en el marcador score_multiplier_ = 1.0F; setScoreboardMode(Scoreboard::Mode::SCORE); break; } case State::RESPAWNING: { playSound("voice_thankyou.wav"); setPlayingState(State::PLAYING); setInvulnerable(true); break; } case State::PLAYING: { init(); setInvulnerable(true); setScoreboardMode(Scoreboard::Mode::SCORE); stage_info_->enablePowerCollection(); break; } case State::CONTINUE: { // Inicializa el contador de continuar continue_counter_ = 9; continue_time_accumulator_ = 0.0F; // Initialize time accumulator playSound("continue_clock.wav"); setScoreboardMode(Scoreboard::Mode::CONTINUE); break; } case State::WAITING: { switch (id_) { case Id::PLAYER1: pos_x_ = param.game.game_area.rect.x; break; case Id::PLAYER2: pos_x_ = param.game.game_area.rect.w - WIDTH; break; default: pos_x_ = 0; break; } pos_y_ = default_pos_y_; waiting_counter_ = 0; waiting_time_accumulator_ = 0.0F; // Initialize time accumulator shiftSprite(); player_sprite_->setCurrentAnimation("hello"); player_sprite_->animtionPause(); setScoreboardMode(Scoreboard::Mode::WAITING); break; } case State::ENTERING_NAME: { setScoreboardMode(Scoreboard::Mode::SCORE_TO_ENTER_NAME); // Iniciar animación de transición break; } case State::SHOWING_NAME: { showing_name_time_accumulator_ = 0.0F; // Inicializar acumulador time-based setScoreboardMode(Scoreboard::Mode::ENTER_TO_SHOW_NAME); // Iniciar animación de transición Scoreboard::get()->setEnterName(scoreboard_panel_, last_enter_name_); addScoreToScoreBoard(); break; } case State::ROLLING: { // Activa la animación de rodar dando botes player_sprite_->setCurrentAnimation("rolling"); player_sprite_->setAnimationSpeed(4.0F / 60.0F); // 4 frames convertido a segundos player_sprite_->setVelY(-396.0F); // Velocidad inicial (6.6 * 60 = 396 pixels/s) player_sprite_->setAccelY(720.0F); // Gravedad (0.2 * 60² = 720 pixels/s²) player_sprite_->setPosY(pos_y_ - 2); // Para "sacarlo" del suelo, ya que está hundido un pixel para ocultar el outline de los pies (rand() % 2 == 0) ? player_sprite_->setVelX(198.0F) : player_sprite_->setVelX(-198.0F); // 3.3 * 60 = 198 pixels/s break; } case State::TITLE_ANIMATION: { // Activa la animación de caminar player_sprite_->setCurrentAnimation("walk"); playSound("voice_credit_thankyou.wav"); break; } case State::TITLE_HIDDEN: { player_sprite_->setVelX(0.0F); player_sprite_->setVelY(0.0F); break; } case State::CONTINUE_TIME_OUT: { // Activa la animación de sacar al jugador de la zona de juego player_sprite_->setAccelY(720.0F); // 0.2 * 60² = 720 pixels/s² player_sprite_->setVelY(-240.0F); // -4.0 * 60 = -240 pixels/s player_sprite_->setVelX(0.0F); player_sprite_->setCurrentAnimation("rolling"); player_sprite_->setAnimationSpeed(5.0F / 60.0F); // 5 frames convertido a segundos setScoreboardMode(Scoreboard::Mode::GAME_OVER); playSound("voice_aw_aw_aw.wav"); playSound("jump.wav"); break; } case State::GAME_OVER: { setScoreboardMode(Scoreboard::Mode::GAME_OVER); break; } case State::CELEBRATING: { game_completed_ = true; setScoreboardMode(Scoreboard::Mode::SCORE); break; } case State::ENTERING_NAME_GAME_COMPLETED: { // setWalkingState(State::WALKING_STOP); // setFiringState(State::FIRING_NONE); setScoreboardMode(Scoreboard::Mode::SCORE_TO_ENTER_NAME); // Iniciar animación de transición break; } case State::LEAVING_SCREEN: { step_counter_ = 0; step_time_accumulator_ = 0.0F; // Initialize time accumulator setScoreboardMode(Scoreboard::Mode::GAME_COMPLETED); break; } case State::ENTERING_SCREEN: { init(); step_counter_ = 0; step_time_accumulator_ = 0.0F; // Initialize time accumulator setScoreboardMode(Scoreboard::Mode::SCORE); switch (id_) { case Id::PLAYER1: pos_x_ = param.game.game_area.rect.x - WIDTH; break; case Id::PLAYER2: pos_x_ = param.game.game_area.rect.x + param.game.game_area.rect.w; break; default: break; } break; } case State::CREDITS: { vel_x_ = (walking_state_ == State::WALKING_RIGHT) ? BASE_SPEED : -BASE_SPEED; break; } default: break; } } // Aumenta el valor de la variable hasta un máximo void Player::incScoreMultiplier() { score_multiplier_ += 0.1F; score_multiplier_ = std::min(score_multiplier_, 5.0F); } // Decrementa el valor de la variable hasta un mínimo void Player::decScoreMultiplier() { score_multiplier_ -= 0.1F; score_multiplier_ = std::max(score_multiplier_, 1.0F); } // Establece el valor del estado void Player::setInvulnerable(bool value) { invulnerable_ = value; invulnerable_counter_ = invulnerable_ ? INVULNERABLE_COUNTER : 0; invulnerable_time_accumulator_ = invulnerable_ ? static_cast(INVULNERABLE_COUNTER) / 60.0F : 0.0F; // Convert frames to seconds } // Monitoriza el estado (time-based) void Player::updateInvulnerable(float delta_time) { if (playing_state_ == State::PLAYING && invulnerable_) { if (invulnerable_time_accumulator_ > 0) { invulnerable_time_accumulator_ -= delta_time; // Frecuencia fija de parpadeo adaptada a deltaTime (en segundos) constexpr float BLINK_PERIOD_S = 8.0F / 60.0F; // 8 frames convertidos a segundos // Calcula proporción decreciente basada en tiempo restante const float TOTAL_INVULNERABLE_TIME_S = static_cast(INVULNERABLE_COUNTER) / 60.0F; float progress = 1.0F - (invulnerable_time_accumulator_ / TOTAL_INVULNERABLE_TIME_S); float white_proportion = 0.5F - (progress * 0.2F); // Menos blanco hacia el final // Calcula si debe mostrar textura de invulnerabilidad basado en el ciclo temporal float cycle_position = std::fmod(invulnerable_time_accumulator_, BLINK_PERIOD_S) / BLINK_PERIOD_S; bool should_show_invulnerable = cycle_position < white_proportion; size_t target_texture = should_show_invulnerable ? INVULNERABLE_TEXTURE : coffees_; // Solo cambia textura si es diferente (optimización) if (player_sprite_->getActiveTexture() != target_texture) { player_sprite_->setActiveTexture(target_texture); } } else { // Fin de invulnerabilidad invulnerable_time_accumulator_ = 0; setInvulnerable(false); player_sprite_->setActiveTexture(coffees_); } } } // Establece el valor de la variable void Player::setPowerUp() { power_up_ = true; power_up_counter_ = POWERUP_COUNTER; power_up_time_accumulator_ = static_cast(POWERUP_COUNTER) / 60.0F; // Convert frames to seconds power_sprite_visible_ = true; // Inicialmente visible cuando se activa el power-up in_power_up_ending_phase_ = false; // Empezar en fase normal bullet_color_toggle_ = false; // Resetear toggle } // Actualiza el valor de la variable (time-based) void Player::updatePowerUp(float delta_time) { if (playing_state_ == State::PLAYING) { if (power_up_) { power_up_time_accumulator_ -= delta_time; power_up_ = power_up_time_accumulator_ > 0; if (!power_up_) { power_up_time_accumulator_ = 0; power_sprite_visible_ = false; in_power_up_ending_phase_ = false; bullet_color_toggle_ = false; // Los colores ahora se manejan dinámicamente en getNextBulletColor() } else { // Calcular visibilidad del power sprite const float TOTAL_POWERUP_TIME_S = static_cast(POWERUP_COUNTER) / 60.0F; const float QUARTER_TIME_S = TOTAL_POWERUP_TIME_S / 4.0F; if (power_up_time_accumulator_ > QUARTER_TIME_S) { // En los primeros 75% del tiempo, siempre visible power_sprite_visible_ = true; in_power_up_ending_phase_ = false; } else { // En el último 25%, parpadea cada 20 frames (≈0.333s) constexpr float BLINK_PERIOD_S = 20.0F / 60.0F; constexpr float VISIBLE_PROPORTION = 4.0F / 20.0F; float cycle_position = std::fmod(power_up_time_accumulator_, BLINK_PERIOD_S) / BLINK_PERIOD_S; power_sprite_visible_ = cycle_position >= VISIBLE_PROPORTION; in_power_up_ending_phase_ = true; // Activar modo alternancia de colores de balas } } } else { power_sprite_visible_ = false; in_power_up_ending_phase_ = false; bullet_color_toggle_ = false; } } } // Concede un toque extra al jugador void Player::giveExtraHit() { extra_hit_ = true; if (coffees_ < 2) { coffees_++; player_sprite_->setActiveTexture(coffees_); } } // Quita el toque extra al jugador void Player::removeExtraHit() { if (coffees_ > 0) { coffees_--; setInvulnerable(true); player_sprite_->setActiveTexture(coffees_); } extra_hit_ = coffees_ != 0; } // Actualiza el circulo de colisión a la posición del jugador void Player::shiftColliders() { collider_.x = static_cast(pos_x_ + (WIDTH / 2)); collider_.y = (pos_y_ + (HEIGHT / 2)); } // Pone las texturas del jugador void Player::setPlayerTextures(const std::vector>& texture) { player_sprite_->setTexture(texture[0]); power_sprite_->setTexture(texture[1]); } // Actualiza el contador de continue (time-based) void Player::updateContinueCounter(float delta_time) { if (playing_state_ == State::CONTINUE) { continue_time_accumulator_ += delta_time; constexpr float CONTINUE_INTERVAL_S = 1.0F; // 1 segundo if (continue_time_accumulator_ >= CONTINUE_INTERVAL_S) { continue_time_accumulator_ -= CONTINUE_INTERVAL_S; decContinueCounter(); Scoreboard::get()->triggerPanelPulse(scoreboard_panel_, 0.2F); } } } // Actualiza el contador de entrar nombre (time-based) void Player::updateEnterNameCounter(float delta_time) { if (playing_state_ == State::ENTERING_NAME || playing_state_ == State::ENTERING_NAME_GAME_COMPLETED) { name_entry_time_accumulator_ += delta_time; constexpr float NAME_ENTRY_INTERVAL_S = 1.0F; // 1 segundo if (name_entry_time_accumulator_ >= NAME_ENTRY_INTERVAL_S) { name_entry_time_accumulator_ -= NAME_ENTRY_INTERVAL_S; decNameEntryCounter(); } } } // Actualiza el estado de SHOWING_NAME (time-based) void Player::updateShowingName(float delta_time) { if (playing_state_ == State::SHOWING_NAME) { showing_name_time_accumulator_ += delta_time; constexpr float SHOWING_NAME_DURATION_S = 5.0F; // 5 segundos if (showing_name_time_accumulator_ >= SHOWING_NAME_DURATION_S) { game_completed_ ? setPlayingState(State::LEAVING_SCREEN) : setPlayingState(State::CONTINUE); } } } // Decrementa el contador de continuar void Player::decContinueCounter() { continue_time_accumulator_ = 0.0F; // Reset time accumulator --continue_counter_; if (continue_counter_ < 0) { setPlayingState(State::CONTINUE_TIME_OUT); } else { playSound("continue_clock.wav"); } } // Decrementa el contador de entrar nombre void Player::decNameEntryCounter() { name_entry_time_accumulator_ = 0.0F; // Reset time accumulator // Incrementa acumuladores de tiempo (1 segundo) name_entry_idle_time_accumulator_ += 1.0F; name_entry_total_time_accumulator_ += 1.0F; if ((name_entry_total_time_accumulator_ >= param.game.name_entry_total_time) || (name_entry_idle_time_accumulator_ >= param.game.name_entry_idle_time)) { name_entry_total_time_accumulator_ = 0.0F; name_entry_idle_time_accumulator_ = 0.0F; if (playing_state_ == State::ENTERING_NAME) { last_enter_name_ = getRecordName(); setPlayingState(State::SHOWING_NAME); } else { setPlayingState(State::LEAVING_SCREEN); } } } // Recoloca los sprites void Player::shiftSprite() { player_sprite_->setPosX(pos_x_); player_sprite_->setPosY(pos_y_); power_sprite_->setPosX(getPosX() - power_up_x_offset_); } // Hace sonar un sonido void Player::playSound(const std::string& name) const { if (demo_) { return; } static auto* audio_ = Audio::get(); audio_->playSound(name); } // Indica si se puede dibujar el objeto auto Player::isRenderable() const -> bool { return !isTitleHidden(); }; // Devuelve el color actual de bala según el estado auto Player::getBulletColor() const -> Bullet::Color { return power_up_ ? bullet_colors_.powered_color : bullet_colors_.normal_color; } // Devuelve el color para la próxima bala (alterna si está en modo toggle) auto Player::getNextBulletColor() -> Bullet::Color { if (in_power_up_ending_phase_) { // En fase final: alternar entre colores powered y normal bullet_color_toggle_ = !bullet_color_toggle_; return bullet_color_toggle_ ? bullet_colors_.powered_color : bullet_colors_.normal_color; } // Modo normal: sin power-up = normal_color, con power-up = powered_color return power_up_ ? bullet_colors_.powered_color : bullet_colors_.normal_color; } // Establece los colores de bala para este jugador void Player::setBulletColors(Bullet::Color normal, Bullet::Color powered) { bullet_colors_.normal_color = normal; bullet_colors_.powered_color = powered; } // Establece el archivo de sonido de bala para este jugador void Player::setBulletSoundFile(const std::string& filename) { bullet_sound_file_ = filename; } // Añade una puntuación a la tabla de records void Player::addScoreToScoreBoard() const { if (hi_score_table_ == nullptr) { return; // Verificar esto antes de crear el manager } const auto ENTRY = HiScoreEntry(trim(getLastEnterName()), getScore(), get1CC()); auto manager = std::make_unique(*hi_score_table_); if (glowing_entry_ != nullptr) { *glowing_entry_ = manager->add(ENTRY); } manager->saveToFile(Asset::get()->get("score.bin")); } void Player::addCredit() { ++credits_used_; playSound("credit.wav"); } // ======================================== // SISTEMA DE DISPARO DE DOS LÍNEAS // ======================================== // Método principal del sistema de disparo void Player::updateFireSystem(float delta_time) { updateFunctionalLine(delta_time); // Línea 1: CanFire updateVisualLine(delta_time); // Línea 2: Animaciones } // LÍNEA 1: Sistema Funcional (CanFire) void Player::updateFunctionalLine(float delta_time) { if (fire_cooldown_timer_ > 0) { fire_cooldown_timer_ -= delta_time; can_fire_new_system_ = false; } else { fire_cooldown_timer_ = 0; // Evitar valores negativos can_fire_new_system_ = true; } } // LÍNEA 2: Sistema Visual (Animaciones) void Player::updateVisualLine(float delta_time) { if (visual_fire_state_ == VisualFireState::NORMAL) { return; // No hay temporizador activo en estado NORMAL } visual_state_timer_ -= delta_time; switch (visual_fire_state_) { case VisualFireState::AIMING: if (visual_state_timer_ <= 0) { transitionToRecoilingNew(); } break; case VisualFireState::RECOILING: if (visual_state_timer_ <= 0) { transitionToThreatPose(); } break; case VisualFireState::THREAT_POSE: if (visual_state_timer_ <= 0) { transitionToNormalNew(); } break; case VisualFireState::NORMAL: // Ya manejado arriba break; } } // Inicia un disparo en ambas líneas void Player::startFiringSystem(int cooldown_frames) { // LÍNEA 1: Inicia cooldown funcional fire_cooldown_timer_ = static_cast(cooldown_frames) / 60.0F; // Convertir frames a segundos can_fire_new_system_ = false; // LÍNEA 2: Resetea completamente el estado visual aiming_duration_ = fire_cooldown_timer_ * AIMING_DURATION_FACTOR; // 50% del cooldown recoiling_duration_ = aiming_duration_ * RECOILING_DURATION_MULTIPLIER; // 4 veces la duración de aiming visual_fire_state_ = VisualFireState::AIMING; visual_state_timer_ = aiming_duration_; updateFiringStateFromVisual(); // Sincroniza firing_state_ para animaciones } // Sincroniza firing_state_ con visual_fire_state_ void Player::updateFiringStateFromVisual() { // Mantener la dirección actual del disparo State base_state = State::FIRING_NONE; if (firing_state_ == State::FIRING_LEFT || firing_state_ == State::RECOILING_LEFT || firing_state_ == State::COOLING_LEFT) { base_state = State::FIRING_LEFT; } else if (firing_state_ == State::FIRING_RIGHT || firing_state_ == State::RECOILING_RIGHT || firing_state_ == State::COOLING_RIGHT) { base_state = State::FIRING_RIGHT; } else if (firing_state_ == State::FIRING_UP || firing_state_ == State::RECOILING_UP || firing_state_ == State::COOLING_UP) { base_state = State::FIRING_UP; } switch (visual_fire_state_) { case VisualFireState::NORMAL: firing_state_ = State::FIRING_NONE; break; case VisualFireState::AIMING: firing_state_ = base_state; // FIRING_LEFT/RIGHT/UP break; case VisualFireState::RECOILING: switch (base_state) { case State::FIRING_LEFT: firing_state_ = State::RECOILING_LEFT; break; case State::FIRING_RIGHT: firing_state_ = State::RECOILING_RIGHT; break; case State::FIRING_UP: firing_state_ = State::RECOILING_UP; break; default: firing_state_ = State::RECOILING_UP; break; } break; case VisualFireState::THREAT_POSE: switch (base_state) { case State::FIRING_LEFT: firing_state_ = State::COOLING_LEFT; break; case State::FIRING_RIGHT: firing_state_ = State::COOLING_RIGHT; break; case State::FIRING_UP: firing_state_ = State::COOLING_UP; break; default: firing_state_ = State::COOLING_UP; break; } break; } } // Transiciones del sistema visual void Player::transitionToRecoilingNew() { visual_fire_state_ = VisualFireState::RECOILING; visual_state_timer_ = recoiling_duration_; updateFiringStateFromVisual(); } void Player::transitionToThreatPose() { visual_fire_state_ = VisualFireState::THREAT_POSE; // Calcular threat_pose_duration ajustada: // Duración original (833ms) menos el tiempo extra que ahora dura recoiling float original_recoiling_duration = fire_cooldown_timer_; // Era 100% del cooldown float new_recoiling_duration = aiming_duration_ * RECOILING_DURATION_MULTIPLIER; // Ahora es más del cooldown float extra_recoiling_time = new_recoiling_duration - original_recoiling_duration; float adjusted_threat_duration = THREAT_POSE_DURATION - extra_recoiling_time; // Asegurar que no sea negativo visual_state_timer_ = std::max(adjusted_threat_duration, MIN_THREAT_POSE_DURATION); updateFiringStateFromVisual(); } void Player::transitionToNormalNew() { visual_fire_state_ = VisualFireState::NORMAL; visual_state_timer_ = 0; updateFiringStateFromVisual(); }