Files
projecte_2026/source/game/scenes/game.cpp
2026-04-07 13:42:06 +02:00

1013 lines
38 KiB
C++

#include "game/scenes/game.hpp"
#include <SDL3/SDL.h>
#include <cmath> // Para std::sqrt, std::min
#include <utility>
#include <vector> // Para vector
#include "core/audio/audio.hpp" // Para Audio
#include "core/input/global_inputs.hpp" // Para check
#include "core/input/input.hpp" // Para Input, InputAction, Input::DO_NOT_ALLOW_REPEAT
#include "core/locale/locale.hpp" // Para Locale
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/surface.hpp" // Para Surface
#include "core/rendering/text.hpp" // Para Text, Text::CENTER_FLAG, Text::COLOR_FLAG
#include "core/resources/resource_cache.hpp" // Para ResourceRoom, Resource
#include "core/resources/resource_list.hpp" // Para Asset
#include "core/system/event_buffer.hpp" // Para EventBuffer
#include "core/system/global_events.hpp" // Para check
#include "game/defaults.hpp" // Para Defaults::Game
#include "game/game_control.hpp" // Para GameControl
#include "game/gameplay/item_tracker.hpp" // Para ItemTracker
#include "game/gameplay/room.hpp" // Para Room, RoomData
#include "game/gameplay/room_tracker.hpp" // Para RoomTracker
#include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data, Scoreboard
#include "game/options.hpp" // Para Options, options, Cheat, SectionState
#include "game/scene_manager.hpp" // Para SceneManager
#include "game/ui/console.hpp" // Para Console
#include "game/ui/notifier.hpp" // Para Notifier, NotificationText, CHEEVO_NO...
#include "utils/defines.hpp" // Para Tile::SIZE, PlayArea::HEIGHT, RoomBorder::BOTTOM
#include "utils/utils.hpp"
#ifdef _DEBUG
#include "core/system/debug.hpp" // Para Debug
#include "game/editor/map_editor.hpp" // Para MapEditor
#endif
// Constructor
Game::Game(Mode mode)
: scoreboard_data_(std::make_shared<Scoreboard::Data>(0, 9, 0, true, 0, SDL_GetTicks())),
scoreboard_(std::make_shared<Scoreboard>(scoreboard_data_)),
room_tracker_(std::make_shared<RoomTracker>()),
mode_(mode),
#ifdef _DEBUG
current_room_(Debug::get()->getSpawnSettings().room),
spawn_data_(Player::SpawnData(Debug::get()->getSpawnSettings().spawn_x, Debug::get()->getSpawnSettings().spawn_y, 0, 0, 0, Player::State::ON_GROUND, Debug::get()->getSpawnSettings().flip)) {
#else
current_room_(Defaults::Game::Room::INITIAL),
spawn_data_(Player::SpawnData(Defaults::Game::Player::SPAWN_X, Defaults::Game::Player::SPAWN_Y, 0, 0, 0, Player::State::ON_GROUND, Defaults::Game::Player::SPAWN_FLIP)) {
#endif
#ifdef _DEBUG
// Validar que la room de debug existe; si no, fallback a la default
if (Resource::List::get()->get(current_room_).empty()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Debug room %s not found, using default", current_room_.c_str());
current_room_ = Defaults::Game::Room::INITIAL;
spawn_data_ = Player::SpawnData(Defaults::Game::Player::SPAWN_X, Defaults::Game::Player::SPAWN_Y, 0, 0, 0, Player::State::ON_GROUND, Defaults::Game::Player::SPAWN_FLIP);
auto ss = Debug::get()->getSpawnSettings();
ss.room = current_room_;
Debug::get()->setSpawnSettings(ss);
Debug::get()->saveToFile();
}
#endif
// Crea objetos e inicializa variables
ItemTracker::init();
demoInit();
room_ = std::make_shared<Room>(current_room_, scoreboard_data_);
initPlayer(spawn_data_, room_);
game_backbuffer_surface_ = std::make_shared<Surface>(Options::game.width, Options::game.height);
changeRoom(current_room_);
#ifdef _DEBUG
Console::get()->setScope("debug");
#else
Console::get()->setScope("game");
#endif
Console::get()->on_toggle = [this](bool open) { player_->setIgnoreInput(open); };
if (Console::get()->isActive()) { player_->setIgnoreInput(true); }
GameControl::change_player_skin = [this](const std::string& skin_name) -> void {
Options::game.player_skin = skin_name;
player_->setSkin(skin_name);
};
GameControl::change_player_color = [this](int color) -> void {
Options::game.player_color = color;
player_->setColor();
};
#ifdef _DEBUG
GameControl::change_room = [this](const std::string& r) -> bool { return this->changeRoom(r); };
GameControl::get_current_room = [this]() -> std::string { return current_room_; };
GameControl::set_items = [this](int count) -> void {
scoreboard_data_->items = count;
Options::stats.items = count;
};
GameControl::toggle_debug_mode = [this]() -> void {
const bool ENTERING_DEBUG = !Debug::get()->isEnabled();
if (ENTERING_DEBUG) {
invincible_before_debug_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED);
}
Debug::get()->toggleEnabled();
room_->redrawMap();
if (Debug::get()->isEnabled()) {
Options::cheats.invincible = Options::Cheat::State::ENABLED;
} else {
Options::cheats.invincible = invincible_before_debug_ ? Options::Cheat::State::ENABLED : Options::Cheat::State::DISABLED;
}
player_->setColor();
scoreboard_data_->music = !Debug::get()->isEnabled();
scoreboard_data_->music ? Audio::get()->resumeMusic() : Audio::get()->pauseMusic();
};
GameControl::set_initial_room = [this]() -> std::string {
auto ss = Debug::get()->getSpawnSettings();
ss.room = current_room_;
Debug::get()->setSpawnSettings(ss);
Debug::get()->saveToFile();
const std::string ROOM_NUM = ss.room.substr(0, ss.room.find('.'));
return "Room:" + ROOM_NUM;
};
GameControl::set_initial_pos = [this]() -> std::string {
auto rect = player_->getRect();
int tile_x = static_cast<int>((rect.x + (rect.w / 2.0F)) / Tile::SIZE);
int tile_y = static_cast<int>(rect.y / Tile::SIZE);
auto ss = Debug::get()->getSpawnSettings();
ss.spawn_x = tile_x * Tile::SIZE;
ss.spawn_y = tile_y * Tile::SIZE;
ss.flip = player_->getSpawnParams().flip;
Debug::get()->setSpawnSettings(ss);
Debug::get()->saveToFile();
return "Pos:" + std::to_string(tile_x) + "," + std::to_string(tile_y);
};
GameControl::enter_editor = [this]() -> void {
MapEditor::get()->enter(room_, player_, current_room_, scoreboard_data_);
};
GameControl::exit_editor = [this]() -> void {
MapEditor::get()->exit();
// Recargar la habitación desde disco (con los cambios del editor)
Resource::Cache::get()->reloadRoom(current_room_);
changeRoom(current_room_);
player_->setRoom(room_);
};
GameControl::revert_editor = []() -> std::string {
return MapEditor::get()->revert();
};
GameControl::get_adjacent_room = [this](const std::string& direction) -> std::string {
if (direction == "UP") { return room_->getRoom(Room::Border::TOP); }
if (direction == "DOWN") { return room_->getRoom(Room::Border::BOTTOM); }
if (direction == "LEFT") { return room_->getRoom(Room::Border::LEFT); }
if (direction == "RIGHT") { return room_->getRoom(Room::Border::RIGHT); }
return "0";
};
#endif
SceneManager::current = SceneManager::Scene::GAME;
SceneManager::options = SceneManager::Options::NONE;
// Inicialización de música (antes estaba al principio de run())
keepMusicPlaying();
if (!scoreboard_data_->music && mode_ == Mode::GAME) {
Audio::get()->pauseMusic();
}
}
Game::~Game() {
// Parar música al destruir la escena (antes estaba al final de run())
if (mode_ == Mode::GAME) {
Audio::get()->stopMusic();
}
ItemTracker::destroy();
GameControl::change_player_skin = nullptr;
GameControl::change_player_color = nullptr;
Console::get()->on_toggle = nullptr;
#ifdef _DEBUG
GameControl::change_room = nullptr;
GameControl::get_current_room = nullptr;
GameControl::set_items = nullptr;
GameControl::toggle_debug_mode = nullptr;
GameControl::set_initial_room = nullptr;
GameControl::set_initial_pos = nullptr;
if (MapEditor::get()->isActive()) { MapEditor::get()->exit(); }
GameControl::enter_editor = nullptr;
GameControl::exit_editor = nullptr;
GameControl::revert_editor = nullptr;
GameControl::reload_current_room = nullptr;
GameControl::get_adjacent_room = nullptr;
#endif
}
// Comprueba los eventos de la cola
void Game::handleEvents() {
for (const auto& event : EventBuffer::events) {
GlobalEvents::handle(event);
#ifdef _DEBUG
// En modo editor: click del ratón cierra la consola
if (Console::get()->isActive() && MapEditor::get()->isActive() &&
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
Console::get()->toggle();
}
if (!Console::get()->isActive()) {
// Tecla 9: toggle editor (funciona tanto dentro como fuera del editor)
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_9 && static_cast<int>(event.key.repeat) == 0) {
if (MapEditor::get()->isActive()) {
GameControl::exit_editor();
Notifier::get()->show({Locale::get()->get("game.editor_disabled")}); // NOLINT(readability-static-accessed-through-instance)
} else {
GameControl::enter_editor();
Notifier::get()->show({Locale::get()->get("game.editor_enabled")}); // NOLINT(readability-static-accessed-through-instance)
}
} else if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_8 && static_cast<int>(event.key.repeat) == 0 && MapEditor::get()->isActive()) {
MapEditor::get()->showGrid(!MapEditor::get()->isGridEnabled());
} else if (MapEditor::get()->isActive()) {
MapEditor::get()->handleEvent(event);
} else {
handleDebugEvents(event);
}
}
#endif
}
}
// Comprueba el teclado
void Game::handleInput() {
Input::get()->update();
// Inputs globales siempre funcionan
if (Input::get()->checkAction(InputAction::TOGGLE_IN_GAME_MUSIC, Input::DO_NOT_ALLOW_REPEAT)) {
scoreboard_data_->music = !scoreboard_data_->music;
scoreboard_data_->music ? Audio::get()->resumeMusic() : Audio::get()->pauseMusic();
Notifier::get()->show({scoreboard_data_->music ? Locale::get()->get("game.music_enabled") : Locale::get()->get("game.music_disabled")});
}
// Si la consola está activa, no procesar inputs del juego
if (Console::get()->isActive()) {
GlobalInputs::handle();
return;
}
#ifdef _DEBUG
// Si el editor de mapas está activo, no procesar inputs del juego
if (MapEditor::get()->isActive()) {
GlobalInputs::handle();
return;
}
#endif
// Durante fade/postfade, solo procesar inputs globales
if (state_ != State::PLAYING) {
GlobalInputs::handle();
return;
}
// Input de pausa solo en estado PLAYING
if (Input::get()->checkAction(InputAction::PAUSE, Input::DO_NOT_ALLOW_REPEAT)) { // NOLINT(readability-static-accessed-through-instance)
togglePause();
Notifier::get()->show({paused_ ? Locale::get()->get("game.paused") : Locale::get()->get("game.running")}); // NOLINT(readability-static-accessed-through-instance)
}
GlobalInputs::handle();
}
// Actualiza el juego, las variables, comprueba la entrada, etc.
void Game::update() {
const float DELTA_TIME = delta_timer_.tick();
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
#ifdef _DEBUG
Debug::get()->clear();
#endif
// Dispatch por estado
switch (state_) {
case State::PLAYING:
updatePlaying(DELTA_TIME);
break;
case State::BLACK_SCREEN:
updateBlackScreen(DELTA_TIME);
break;
case State::GAME_OVER:
updateGameOver(DELTA_TIME);
break;
case State::FADE_TO_ENDING:
updateFadeToEnding(DELTA_TIME);
break;
case State::POST_FADE_ENDING:
updatePostFadeEnding(DELTA_TIME);
break;
}
Audio::update(); // Actualiza el objeto Audio
Screen::get()->update(DELTA_TIME); // Actualiza el objeto Screen
}
// Actualiza el juego en estado PLAYING
void Game::updatePlaying(float delta_time) {
#ifdef _DEBUG
// Si el editor de mapas está activo, delegar en él y no ejecutar gameplay
if (MapEditor::get()->isActive()) {
MapEditor::get()->update(delta_time);
return;
}
#endif
// Actualiza los objetos
room_->update(delta_time);
if (transitioning_ && transition_adjacent_room_) {
transition_adjacent_room_->update(delta_time);
}
switch (mode_) {
case Mode::GAME:
#ifdef _DEBUG
// Maneja el arrastre del jugador con el ratón (debug)
handleDebugMouseDrag(delta_time);
// Si estamos arrastrando, no ejecutar la física normal del jugador
if (!debug_dragging_player_) {
player_->update(delta_time);
}
#else
player_->update(delta_time);
#endif
checkPlayerIsOnBorder();
checkPlayerAndItems();
checkPlayerAndEnemies();
checkIfPlayerIsAlive();
// Actualizar cámara de transición
if (transitioning_) {
updateTransitionCamera(delta_time);
}
break;
case Mode::DEMO:
demoCheckRoomChange(delta_time);
break;
}
scoreboard_->update(delta_time);
keepMusicPlaying();
}
// Actualiza el juego en estado BLACK_SCREEN
void Game::updateBlackScreen(float delta_time) {
state_time_ += delta_time;
// Si se acabaron las vidas Y pasó el threshold → GAME_OVER
if (scoreboard_data_->lives < 0 && state_time_ > GAME_OVER_THRESHOLD) {
transitionToState(State::GAME_OVER);
return;
}
// Si pasó la duración completa → volver a PLAYING
if (state_time_ > BLACK_SCREEN_DURATION) {
// Despausar al salir
player_->setPaused(false);
room_->setPaused(false);
Screen::get()->setBorderColor(room_->getBorderColor());
transitionToState(State::PLAYING);
}
}
// Actualiza el juego en estado GAME_OVER
void Game::updateGameOver(float delta_time) {
// Pequeño delay antes de cambiar escena
state_time_ += delta_time;
if (state_time_ > 0.1F) { // 100ms de delay mínimo
SceneManager::current = SceneManager::Scene::TITLE;
}
}
// Actualiza el juego en estado FADE_TO_ENDING
void Game::updateFadeToEnding(float delta_time) {
// Actualiza room, enemies, items (todo sigue funcionando)
room_->update(delta_time);
// NO actualizar player (congelar movimiento)
// player_->update(delta_time); -- COMENTADO INTENCIONALMENTE
// Actualiza scoreboard
scoreboard_->update(delta_time);
keepMusicPlaying();
// Aplica el fade progresivo al BACKBUFFER (no al renderer de pantalla)
fade_accumulator_ += delta_time;
if (fade_accumulator_ >= FADE_STEP_INTERVAL) {
fade_accumulator_ = 0.0F;
if (game_backbuffer_surface_->fadeSubPalette()) {
// Fade completado, transicionar a POST_FADE
transitionToState(State::POST_FADE_ENDING);
}
}
}
// Actualiza el juego en estado POST_FADE_ENDING
void Game::updatePostFadeEnding(float delta_time) {
// Pantalla negra estática, acumular tiempo
state_time_ += delta_time;
// Después del delay, cambiar a la escena de ending
if (state_time_ >= POST_FADE_DELAY) {
SceneManager::current = SceneManager::Scene::TITLE;
}
}
// Cambia al estado especificado y resetea los timers
void Game::transitionToState(State new_state) {
// Limpiar transición de pantalla si estaba activa
if (transitioning_) {
endTransition();
}
// Lógica de ENTRADA según el nuevo estado
if (new_state == State::BLACK_SCREEN) {
// Respawn room y player
room_ = std::make_shared<Room>(current_room_, scoreboard_data_);
initPlayer(spawn_data_, room_);
// Pausar ambos
room_->setPaused(true);
player_->setPaused(true);
}
state_ = new_state;
state_time_ = 0.0F;
fade_accumulator_ = 0.0F;
}
// Pinta los objetos en pantalla
void Game::render() {
// Dispatch por estado
switch (state_) {
case State::PLAYING:
renderPlaying();
break;
case State::BLACK_SCREEN:
renderBlackScreen();
break;
case State::GAME_OVER:
renderGameOver();
break;
case State::FADE_TO_ENDING:
renderFadeToEnding();
break;
case State::POST_FADE_ENDING:
renderPostFadeEnding();
break;
}
}
// Renderiza el juego en estado PLAYING (directo a pantalla)
void Game::renderPlaying() {
// Prepara para dibujar el frame
Screen::get()->start();
#ifdef _DEBUG
// Si el editor está activo, delegar el renderizado de entidades y statusbar
if (MapEditor::get()->isActive()) {
room_->renderMap();
MapEditor::get()->render();
Screen::get()->render();
return;
}
#endif
if (transitioning_) {
// --- Transición animada entre pantallas ---
int cam_x = static_cast<int>(camera_offset_x_);
int cam_y = static_cast<int>(camera_offset_y_);
// Renderizar habitación principal con offset de cámara
Screen::get()->setRenderOffset(cam_x, cam_y);
room_->renderMap();
room_->renderEnemies();
room_->renderItems();
if (mode_ == Mode::GAME) {
player_->render();
}
// Renderizar habitación adyacente: misma cámara pero desplazada una pantalla
if (transition_adjacent_room_) {
int adj_x = cam_x;
int adj_y = cam_y;
switch (transition_direction_) {
case Room::Border::TOP: adj_y += -PlayArea::HEIGHT; break;
case Room::Border::BOTTOM: adj_y += PlayArea::HEIGHT; break;
case Room::Border::LEFT: adj_x += -PlayArea::WIDTH; break;
case Room::Border::RIGHT: adj_x += PlayArea::WIDTH; break;
default: break;
}
Screen::get()->setRenderOffset(adj_x, adj_y);
transition_adjacent_room_->renderMap();
transition_adjacent_room_->renderEnemies();
transition_adjacent_room_->renderItems();
}
// Scoreboard sin offset
Screen::get()->setRenderOffset(0, 0);
scoreboard_->render();
} else {
// --- Renderizado normal ---
room_->renderMap();
room_->renderEnemies();
room_->renderItems();
if (mode_ == Mode::GAME) {
player_->render();
}
scoreboard_->render();
}
#ifdef _DEBUG
// Debug info
renderDebugInfo();
#endif
// Actualiza la pantalla
Screen::get()->render();
}
// Renderiza el juego en estado BLACK_SCREEN (pantalla negra)
void Game::renderBlackScreen() {
Screen::get()->start();
auto const COLOR = 0;
Screen::get()->clearSurface(COLOR);
Screen::get()->setBorderColor(COLOR);
Screen::get()->render();
}
// Renderiza el juego en estado GAME_OVER (pantalla negra)
void Game::renderGameOver() {
Screen::get()->start();
Screen::get()->clearSurface(0);
Screen::get()->render();
}
// Renderiza el juego en estado FADE_TO_ENDING (via backbuffer)
void Game::renderFadeToEnding() {
// 1. Guardar renderer actual
auto previous_renderer = Screen::get()->getRendererSurface();
// 2. Cambiar target a backbuffer
Screen::get()->setRendererSurface(game_backbuffer_surface_);
// 3. Renderizar todo a backbuffer
game_backbuffer_surface_->clear(0);
room_->renderMap();
room_->renderEnemies();
room_->renderItems();
player_->render(); // Player congelado pero visible
scoreboard_->render();
// 4. Restaurar renderer original
Screen::get()->setRendererSurface(previous_renderer);
// 5. Preparar pantalla y volcar backbuffer (fade YA aplicado en update)
Screen::get()->start();
game_backbuffer_surface_->render();
Screen::get()->render();
}
// Renderiza el juego en estado POST_FADE_ENDING (pantalla negra)
void Game::renderPostFadeEnding() {
Screen::get()->start();
Screen::get()->clearSurface(0);
Screen::get()->render();
}
#ifdef _DEBUG
// Helper: alterna un cheat y muestra notificación con su estado
static void toggleCheat(Options::Cheat::State& cheat, const std::string& label) {
cheat = (cheat == Options::Cheat::State::ENABLED) ? Options::Cheat::State::DISABLED : Options::Cheat::State::ENABLED;
const bool ENABLED = (cheat == Options::Cheat::State::ENABLED);
Notifier::get()->show({label + (ENABLED ? Locale::get()->get("game.enabled") : Locale::get()->get("game.disabled"))}, Notifier::Style::DEFAULT, -1, true); // NOLINT(readability-static-accessed-through-instance)
}
// Pone la información de debug en pantalla
void Game::renderDebugInfo() {
if (!Debug::get()->isEnabled()) { return; }
auto surface = Screen::get()->getRendererSurface();
// Pinta la rejilla
/*for (int i = 0; i < PlayArea::BOTTOM; i += 8)
{
// Lineas horizontales
surface->drawLine(0, i, PlayArea::RIGHT, i, 1);
}
for (int i = 0; i < PlayArea::RIGHT; i += 8)
{
// Lineas verticales
surface->drawLine(i, 0, i, PlayArea::BOTTOM - 1, 1);
}*/
// Pinta el texto
Debug::get()->setPos({.x = 1, .y = 18 * 8});
Debug::get()->render();
}
// Comprueba los eventos
void Game::handleDebugEvents(const SDL_Event& event) { // NOLINT(readability-convert-member-functions-to-static)
if (event.type == SDL_EVENT_KEY_DOWN && static_cast<int>(event.key.repeat) == 0) {
switch (event.key.key) {
case SDLK_R:
Resource::Cache::get()->reload();
break;
case SDLK_W:
changeRoom(room_->getRoom(Room::Border::TOP));
break;
case SDLK_A:
changeRoom(room_->getRoom(Room::Border::LEFT));
break;
case SDLK_S:
changeRoom(room_->getRoom(Room::Border::BOTTOM));
break;
case SDLK_D:
changeRoom(room_->getRoom(Room::Border::RIGHT));
break;
case SDLK_1:
toggleCheat(Options::cheats.infinite_lives, Locale::get()->get("game.cheat_infinite_lives")); // NOLINT(readability-static-accessed-through-instance)
player_->setColor();
break;
case SDLK_2:
toggleCheat(Options::cheats.invincible, Locale::get()->get("game.cheat_invincible")); // NOLINT(readability-static-accessed-through-instance)
player_->setColor();
break;
case SDLK_7:
Notifier::get()->show({Locale::get()->get("achievements.header"), Locale::get()->get("achievements.c11")}, Notifier::Style::CHEEVO, -1, false, "F7"); // NOLINT(readability-static-accessed-through-instance)
break;
case SDLK_0: {
const bool ENTERING_DEBUG = !Debug::get()->isEnabled();
if (ENTERING_DEBUG) {
invincible_before_debug_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED);
}
Debug::get()->toggleEnabled();
Notifier::get()->show({Debug::get()->isEnabled() ? Locale::get()->get("game.debug_enabled") : Locale::get()->get("game.debug_disabled")}); // NOLINT(readability-static-accessed-through-instance)
room_->redrawMap();
if (Debug::get()->isEnabled()) {
Options::cheats.invincible = Options::Cheat::State::ENABLED;
} else {
Options::cheats.invincible = invincible_before_debug_ ? Options::Cheat::State::ENABLED : Options::Cheat::State::DISABLED;
}
player_->setColor();
scoreboard_data_->music = !Debug::get()->isEnabled();
scoreboard_data_->music ? Audio::get()->resumeMusic() : Audio::get()->pauseMusic();
break;
}
default:
break;
}
}
}
// Maneja el arrastre del jugador con el ratón (debug)
void Game::handleDebugMouseDrag(float delta_time) {
// Solo funciona si Debug está habilitado
if (!Debug::get()->isEnabled()) {
return;
}
// Obtener estado del ratón (coordenadas de ventana física)
float mouse_x = 0.0F;
float mouse_y = 0.0F;
SDL_MouseButtonFlags buttons = SDL_GetMouseState(&mouse_x, &mouse_y);
// Convertir coordenadas de ventana a coordenadas lógicas del renderer
float render_x = 0.0F;
float render_y = 0.0F;
SDL_RenderCoordinatesFromWindow(Screen::get()->getRenderer(), mouse_x, mouse_y, &render_x, &render_y);
// Restar el offset del borde para obtener coordenadas del área de juego
SDL_FRect dst_rect = Screen::get()->getGameSurfaceDstRect();
float game_x = render_x - dst_rect.x;
float game_y = render_y - dst_rect.y;
// Verificar si los botones están presionados
bool left_button_pressed = (buttons & SDL_BUTTON_LMASK) != 0;
bool right_button_pressed = (buttons & SDL_BUTTON_RMASK) != 0;
// Botón derecho: teleport instantáneo a la posición del cursor
if (right_button_pressed && !debug_dragging_player_) {
player_->setDebugPosition(game_x, game_y);
player_->finalizeDebugTeleport();
} else if (left_button_pressed) {
// Obtener posición actual del jugador
SDL_FRect player_rect = player_->getRect();
float player_x = player_rect.x;
float player_y = player_rect.y;
// Calcular distancia al objetivo
float dx = game_x - player_x;
float dy = game_y - player_y;
float distance = std::sqrt((dx * dx) + (dy * dy));
// Constantes de velocidad con ease-in (aceleración progresiva)
constexpr float DRAG_SPEED_MIN = 30.0F; // Velocidad inicial (pixels/segundo)
constexpr float DRAG_SPEED_MAX = 600.0F; // Velocidad máxima (pixels/segundo)
constexpr float DRAG_ACCELERATION = 600.0F; // Aceleración (pixels/segundo²)
// Incrementar velocidad con el tiempo (ease-in)
if (!debug_dragging_player_) {
debug_drag_speed_ = DRAG_SPEED_MIN; // Iniciar con velocidad mínima
}
debug_drag_speed_ = std::min(DRAG_SPEED_MAX, debug_drag_speed_ + (DRAG_ACCELERATION * delta_time));
if (distance > 1.0F) {
// Calcular el movimiento con la velocidad actual
float move_factor = std::min(1.0F, debug_drag_speed_ * delta_time / distance);
float new_x = player_x + (dx * move_factor);
float new_y = player_y + (dy * move_factor);
// Mover el jugador hacia la posición del cursor
player_->setDebugPosition(new_x, new_y);
}
debug_dragging_player_ = true;
Debug::get()->set("drag.x", std::to_string(static_cast<int>(player_rect.x)));
Debug::get()->set("drag.y", std::to_string(static_cast<int>(player_rect.y)));
} else if (debug_dragging_player_) {
// Botón soltado después de arrastrar: finalizar teleport
player_->finalizeDebugTeleport();
debug_dragging_player_ = false;
debug_drag_speed_ = 0.0F; // Reset para el próximo arrastre
}
}
#endif
// Cambia de habitación
auto Game::changeRoom(const std::string& room_path) -> bool {
// En las habitaciones los limites tienen la cadena del fichero o un 0 en caso de no limitar con nada
if (room_path == "0") {
return false;
}
// Verifica que exista el fichero que se va a cargar
if (!Resource::List::get()->get(room_path).empty()) { // NOLINT(readability-static-accessed-through-instance)
// Crea un objeto habitación nuevo a partir del fichero
room_ = std::make_shared<Room>(room_path, scoreboard_data_);
// Pone el color del marcador en función del color del borde de la habitación
setScoreBoardColor();
if (room_tracker_->addRoom(room_path)) {
// Incrementa el contador de habitaciones visitadas
scoreboard_data_->rooms++;
Options::stats.rooms = scoreboard_data_->rooms;
}
// Pasa la nueva habitación al jugador
player_->setRoom(room_);
// Recalcula el color del jugador (evita coincidir con el fondo)
player_->setColor();
// Cambia la habitación actual
current_room_ = room_path;
return true;
}
return false;
}
// Comprueba si el jugador esta en el borde de la pantalla
void Game::checkPlayerIsOnBorder() {
if (!player_->isOnBorder()) {
// Si hay transición activa y el jugador ha vuelto completamente dentro de bounds,
// comprobar si la cámara también ha vuelto para cancelar la transición
if (transitioning_ && std::abs(camera_offset_x_) < 1.0F && std::abs(camera_offset_y_) < 1.0F) {
endTransition();
}
return;
}
const auto BORDER = player_->getBorder();
// Si ya hay transición activa, comprobar si el jugador hace commit a la room adyacente
if (transitioning_) {
// ¿El jugador ha salido completamente por el lado de la transición?
if (player_->isFullyOutOfBounds()) {
// Commit: la room adyacente pasa a ser la room principal
room_ = transition_adjacent_room_;
player_->setRoom(room_);
player_->commitToRoom(transition_direction_);
current_room_ = transition_adjacent_room_path_;
spawn_data_ = player_->getSpawnParams();
setScoreBoardColor();
// Ajustar cámara: restar el desplazamiento de una pantalla completa
switch (transition_direction_) {
case Room::Border::TOP: camera_offset_y_ -= PlayArea::HEIGHT; break;
case Room::Border::BOTTOM: camera_offset_y_ += PlayArea::HEIGHT; break;
case Room::Border::LEFT: camera_offset_x_ -= PlayArea::WIDTH; break;
case Room::Border::RIGHT: camera_offset_x_ += PlayArea::WIDTH; break;
default: break;
}
// Limpiar transición (pero la cámara sigue animándose hacia 0)
player_->clearAdjacentRoom();
transition_adjacent_room_.reset();
transition_adjacent_room_path_.clear();
transition_direction_ = Room::Border::NONE;
// La cámara aún no está en 0, así que mantenemos transitioning_ = true
// Se resolverá cuando la cámara llegue a 0 y el jugador esté dentro de bounds
if (std::abs(camera_offset_x_) < 1.0F && std::abs(camera_offset_y_) < 1.0F) {
endTransition();
}
}
return;
}
// No hay transición activa — iniciar una nueva
const auto ROOM_NAME = room_->getRoom(BORDER);
// Si no hay habitación adyacente
if (ROOM_NAME == "0") {
if (BORDER == Room::Border::BOTTOM) {
killPlayer();
}
return;
}
// Cargar room adyacente
auto adjacent_room = std::make_shared<Room>(ROOM_NAME, scoreboard_data_);
transition_adjacent_room_ = adjacent_room;
transition_adjacent_room_path_ = ROOM_NAME;
transition_direction_ = BORDER;
// Pasar la room adyacente al player para colisiones
player_->setAdjacentRoom(adjacent_room, BORDER);
// Iniciar transición (NO cambiar room_, NO reposicionar jugador)
transitioning_ = true;
if (room_tracker_->addRoom(ROOM_NAME)) {
scoreboard_data_->rooms++;
Options::stats.rooms = scoreboard_data_->rooms;
}
}
// Actualiza la cámara durante la transición: sigue al jugador con inercia
void Game::updateTransitionCamera(float delta_time) {
// El target es una pantalla completa en la dirección de la transición,
// excepto si el jugador ha vuelto dentro de bounds (target = 0)
float target_x = 0.0F;
float target_y = 0.0F;
const auto RECT = player_->getRect();
const float CENTER_X = RECT.x + (RECT.w / 2.0F);
const float CENTER_Y = RECT.y + (RECT.h / 2.0F);
const bool PLAYER_OUT_OF_BOUNDS =
CENTER_X < PlayArea::LEFT || CENTER_X > PlayArea::RIGHT ||
CENTER_Y < PlayArea::TOP || CENTER_Y > PlayArea::BOTTOM;
if (PLAYER_OUT_OF_BOUNDS) {
// El jugador está fuera: la cámara se dirige a mostrar la room adyacente
switch (transition_direction_) {
case Room::Border::TOP: target_y = static_cast<float>(PlayArea::HEIGHT); break;
case Room::Border::BOTTOM: target_y = -static_cast<float>(PlayArea::HEIGHT); break;
case Room::Border::LEFT: target_x = static_cast<float>(PlayArea::WIDTH); break;
case Room::Border::RIGHT: target_x = -static_cast<float>(PlayArea::WIDTH); break;
default: break;
}
}
// Si el jugador está dentro de bounds, target = 0 → la cámara vuelve
// Interpolar la cámara hacia el objetivo con velocidad constante
constexpr float CAMERA_SPEED = 500.0F;
auto lerp_towards = [delta_time](float current, float target) -> float {
const float DIFF = target - current;
if (std::abs(DIFF) < 1.0F) { return target; }
const float STEP = CAMERA_SPEED * delta_time;
if (std::abs(DIFF) <= STEP) { return target; }
return current + (DIFF > 0.0F ? STEP : -STEP);
};
camera_offset_x_ = lerp_towards(camera_offset_x_, target_x);
camera_offset_y_ = lerp_towards(camera_offset_y_, target_y);
// Si no hay room adyacente pendiente, la cámara vuelve a 0, y al llegar terminamos
if (!transition_adjacent_room_ &&
std::abs(camera_offset_x_) < 1.0F && std::abs(camera_offset_y_) < 1.0F) {
endTransition();
}
}
// Finaliza la transición entre pantallas
void Game::endTransition() {
transitioning_ = false;
camera_offset_x_ = 0.0F;
camera_offset_y_ = 0.0F;
player_->clearAdjacentRoom();
transition_adjacent_room_.reset();
transition_adjacent_room_path_.clear();
transition_direction_ = Room::Border::NONE;
Screen::get()->setRenderOffset(0, 0);
}
// Comprueba las colisiones del jugador con los enemigos
auto Game::checkPlayerAndEnemies() -> bool {
const bool DEATH = room_->enemyCollision(player_->getCollider());
if (DEATH) {
killPlayer();
}
return DEATH;
}
// Comprueba las colisiones del jugador con los objetos
void Game::checkPlayerAndItems() {
room_->itemCollision(player_->getCollider());
}
// Comprueba si el jugador esta vivo
void Game::checkIfPlayerIsAlive() {
if (!player_->isAlive()) {
killPlayer();
}
}
// Mata al jugador
void Game::killPlayer() {
if (Options::cheats.invincible == Options::Cheat::State::ENABLED) {
return;
}
// Resta una vida al jugador
if (Options::cheats.infinite_lives == Options::Cheat::State::DISABLED &&
!(Options::kiosk.enabled && Options::kiosk.infinite_lives)) {
--scoreboard_data_->lives;
}
// Sonido
Audio::get()->playSound("death.wav", Audio::Group::GAME);
// Transicionar al estado BLACK_SCREEN (el respawn ocurre en transitionToState)
transitionToState(State::BLACK_SCREEN);
}
// Pone el color del marcador en función del color del borde de la habitación
void Game::setScoreBoardColor() { // NOLINT(readability-convert-member-functions-to-static)
// Obtiene el color del borde
const Uint8 BORDER_COLOR = room_->getBorderColor();
const bool IS_BLACK = BORDER_COLOR == 0;
const bool IS_BRIGHT_BLACK = BORDER_COLOR == 1;
// Si el color del borde es negro o negro brillante cambia el texto del marcador a blanco
scoreboard_data_->color = IS_BLACK || IS_BRIGHT_BLACK ? 14 : BORDER_COLOR;
}
// Pone el juego en pausa
void Game::togglePause() {
paused_ = !paused_;
player_->setPaused(paused_);
room_->setPaused(paused_);
scoreboard_->setPaused(paused_);
}
// Inicializa al jugador
void Game::initPlayer(const Player::SpawnData& spawn_point, std::shared_ptr<Room> room) { // NOLINT(readability-convert-member-functions-to-static)
const bool IGNORE_INPUT = player_ != nullptr && player_->getIgnoreInput();
std::string player_animations = Player::skinToAnimationPath(Options::game.player_skin);
const Player::Data PLAYER{.spawn_data = spawn_point, .animations_path = player_animations, .room = std::move(room)};
player_ = std::make_shared<Player>(PLAYER);
if (IGNORE_INPUT) { player_->setIgnoreInput(true); }
}
// Hace sonar la música
void Game::keepMusicPlaying() {
const std::string MUSIC_PATH = mode_ == Mode::GAME ? "574070_KUVO_Farewell_to_school.ogg" : "574071_EA_DTV.ogg";
// Si la música no está sonando
if (Audio::get()->getMusicState() == Audio::MusicState::STOPPED) {
Audio::get()->playMusic(MUSIC_PATH);
}
}
// DEMO MODE: Inicializa las variables para el modo demo
void Game::demoInit() {
if (mode_ == Mode::DEMO) {
demo_ = DemoData(0.0F, 0, {"04.yaml", "54.yaml", "20.yaml", "09.yaml", "05.yaml", "11.yaml", "31.yaml", "44.yaml"});
current_room_ = demo_.rooms.front();
}
}
// DEMO MODE: Comprueba si se ha de cambiar de habitación
void Game::demoCheckRoomChange(float delta_time) {
if (mode_ == Mode::DEMO) {
demo_.time_accumulator += delta_time;
if (demo_.time_accumulator >= DEMO_ROOM_DURATION) {
demo_.time_accumulator = 0.0F;
demo_.room_index++;
if (demo_.room_index == (int)demo_.rooms.size()) {
SceneManager::current = SceneManager::Scene::LOGO;
SceneManager::options = SceneManager::Options::LOGO_TO_TITLE;
} else {
changeRoom(demo_.rooms[demo_.room_index]);
}
}
}
}