Files
projecte_2026/source/game/scenes/game.cpp
2026-04-10 14:03:56 +02:00

1044 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/entities/moving_platform.hpp" // Para MovingPlatform
#include "game/game_control.hpp" // Para GameControl
#include "game/gameplay/door_tracker.hpp" // Para DoorTracker
#include "game/gameplay/inventory.hpp" // Para Inventory
#include "game/gameplay/item_tracker.hpp" // Para ItemTracker
#include "game/gameplay/key_tracker.hpp" // Para KeyTracker
#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/easing_functions.hpp" // Para Easing::cubicInOut
#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
// ZoneManager se inicializa en Director (antes que Resource::Cache, que carga rooms)
ItemTracker::init();
KeyTracker::init();
DoorTracker::init();
Inventory::init();
demoInit();
room_ = getOrCreateRoom(current_room_);
initPlayer(spawn_data_, room_);
game_backbuffer_surface_ = std::make_shared<Surface>(Options::game.width, Options::game.height);
changeRoom(current_room_);
buildCollisionBorders();
if (Console::get() != nullptr) {
#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); }
}
#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;
}
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_);
buildCollisionBorders();
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();
KeyTracker::destroy();
DoorTracker::destroy();
Inventory::destroy();
// ZoneManager lo destruye Director
if (Console::get() != 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() != nullptr && 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_old_room_->update(delta_time);
}
updateAdjacentRooms(delta_time);
switch (mode_) {
case Mode::GAME:
// Plataformas: resetear flag y detectar antes de la física del player
player_->clearPlatformFlag();
checkPlayerAndPlatforms();
#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();
checkPlayerAndKeys();
room_->tryUnlockDoors(player_->getCollider());
checkPlayerAndEnemies();
checkIfPlayerIsAlive();
// Avanzar transición
if (transitioning_) {
transition_timer_ += delta_time;
if (transition_timer_ >= TRANSITION_DURATION) {
endTransition();
}
}
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);
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) {
// Limpiar caché de habitaciones (enemigos vuelven a sus posiciones iniciales)
room_cache_.clear();
// Respawn room y player
room_ = getOrCreateRoom(current_room_);
initPlayer(spawn_data_, room_);
buildCollisionBorders();
// 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 ---
const float T = std::min(transition_timer_ / TRANSITION_DURATION, 1.0F);
const float P = Easing::cubicInOut(T);
// Calcular offsets (derivar uno del otro para evitar gap de 1px por truncamiento)
int old_ox = 0;
int old_oy = 0;
int new_ox = 0;
int new_oy = 0;
switch (transition_direction_) {
case Room::Border::RIGHT:
new_ox = static_cast<int>((1.0F - P) * PlayArea::WIDTH);
old_ox = new_ox - PlayArea::WIDTH;
break;
case Room::Border::LEFT:
new_ox = -static_cast<int>((1.0F - P) * PlayArea::WIDTH);
old_ox = new_ox + PlayArea::WIDTH;
break;
case Room::Border::BOTTOM:
new_oy = static_cast<int>((1.0F - P) * PlayArea::HEIGHT);
old_oy = new_oy - PlayArea::HEIGHT;
break;
case Room::Border::TOP:
new_oy = -static_cast<int>((1.0F - P) * PlayArea::HEIGHT);
old_oy = new_oy + PlayArea::HEIGHT;
break;
default:
break;
}
// Renderizar habitación saliente con su offset
Screen::get()->setRenderOffset(old_ox, old_oy);
transition_old_room_->renderMap();
transition_old_room_->renderPlatforms();
transition_old_room_->renderEnemies();
transition_old_room_->renderItems();
transition_old_room_->renderKeys();
transition_old_room_->renderDoors();
// Renderizar habitación entrante + jugador con su offset
Screen::get()->setRenderOffset(new_ox, new_oy);
room_->renderMap();
room_->renderPlatforms();
room_->renderEnemies();
room_->renderItems();
room_->renderKeys();
room_->renderDoors();
if (mode_ == Mode::GAME) {
player_->render();
}
// Scoreboard sin offset
Screen::get()->setRenderOffset(0, 0);
scoreboard_->render();
} else {
// --- Renderizado normal ---
room_->renderMap();
room_->renderPlatforms();
room_->renderEnemies();
room_->renderItems();
room_->renderKeys();
room_->renderDoors();
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();
Screen::get()->clearSurface(0);
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();
room_->renderKeys();
room_->renderDoors();
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)
break;
case SDLK_2:
toggleCheat(Options::cheats.invincible, Locale::get()->get("game.cheat_invincible")); // NOLINT(readability-static-accessed-through-instance)
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;
}
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)
// Obtiene la habitación del caché o la crea
room_ = getOrCreateRoom(room_path);
// Pone el color del marcador en función del color del borde de la habitación
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_);
// Cambia la habitación actual
current_room_ = room_path;
return true;
}
return false;
}
// Obtiene una habitación del caché o la crea si no existe
auto Game::getOrCreateRoom(const std::string& room_path) -> std::shared_ptr<Room> {
auto it = room_cache_.find(room_path);
if (it != room_cache_.end()) {
return it->second;
}
auto room = std::make_shared<Room>(room_path, scoreboard_data_);
room_cache_[room_path] = room;
return room;
}
// Construye el tilemap extendido de la room actual con los bordes de las adyacentes
void Game::buildCollisionBorders() {
// Helper: obtiene el collision tilemap de una room adyacente (nullptr si no existe)
auto getAdjacentCollision = [&](Room::Border b) -> const std::vector<int>* {
auto name = room_->getRoom(b);
if (name == "0") { return nullptr; }
return &getOrCreateRoom(name)->getCollisionTileMap();
};
// Helper: obtiene el collision tilemap de una room diagonal (A→B o C→D)
auto getDiagCollision = [&](Room::Border first, Room::Border second) -> const std::vector<int>* {
// Camino 1: room en dirección 'first', luego su adyacente en dirección 'second'
auto name1 = room_->getRoom(first);
if (name1 != "0") {
auto r = getOrCreateRoom(name1);
auto name2 = r->getRoom(second);
if (name2 != "0") { return &getOrCreateRoom(name2)->getCollisionTileMap(); }
}
// Camino 2: room en dirección 'second', luego su adyacente en dirección 'first'
auto name3 = room_->getRoom(second);
if (name3 != "0") {
auto r = getOrCreateRoom(name3);
auto name4 = r->getRoom(first);
if (name4 != "0") { return &getOrCreateRoom(name4)->getCollisionTileMap(); }
}
return nullptr;
};
CollisionMap::AdjacentData adj;
adj.top = getAdjacentCollision(Room::Border::TOP);
adj.bottom = getAdjacentCollision(Room::Border::BOTTOM);
adj.left = getAdjacentCollision(Room::Border::LEFT);
adj.right = getAdjacentCollision(Room::Border::RIGHT);
adj.top_left = getDiagCollision(Room::Border::TOP, Room::Border::LEFT);
adj.top_right = getDiagCollision(Room::Border::TOP, Room::Border::RIGHT);
adj.bottom_left = getDiagCollision(Room::Border::BOTTOM, Room::Border::LEFT);
adj.bottom_right = getDiagCollision(Room::Border::BOTTOM, Room::Border::RIGHT);
room_->updateCollisionBorders(adj);
}
// Actualiza los enemigos de las habitaciones adyacentes a la actual
void Game::updateAdjacentRooms(float delta_time) {
for (auto border : {Room::Border::TOP, Room::Border::RIGHT, Room::Border::BOTTOM, Room::Border::LEFT}) {
const auto ROOM_NAME = room_->getRoom(border);
if (ROOM_NAME == "0") {
continue;
}
auto adjacent = getOrCreateRoom(ROOM_NAME);
// Evitar actualizar la habitación actual o la que ya se actualiza en la transición
if (adjacent != room_ && adjacent != transition_old_room_) {
adjacent->update(delta_time);
}
}
}
// Comprueba si el jugador esta en el borde de la pantalla
void Game::checkPlayerIsOnBorder() {
// No permitir transiciones encadenadas
if (transitioning_) {
return;
}
if (transition_just_ended_) {
transition_just_ended_ = false;
return;
}
if (player_->isOnBorder()) {
const auto BORDER = player_->getBorder();
const auto ROOM_NAME = room_->getRoom(BORDER);
// Si no hay habitación adyacente
if (ROOM_NAME == "0") {
if (BORDER == Room::Border::BOTTOM) {
killPlayer();
}
return;
}
// Guardar la habitación saliente
transition_old_room_ = room_;
transition_direction_ = BORDER;
// Crear nueva habitación y reposicionar jugador
if (changeRoom(ROOM_NAME)) {
// Construir el tilemap extendido de la nueva room con los bordes adyacentes
buildCollisionBorders();
player_->switchBorders();
spawn_data_ = player_->getSpawnParams();
// Iniciar transición animada (pausar jugador y entidades)
transitioning_ = true;
transition_timer_ = 0.0F;
player_->setPaused(true);
room_->setPaused(true);
transition_old_room_->setPaused(true);
} else {
// changeRoom falló, limpiar
transition_old_room_.reset();
transition_direction_ = Room::Border::NONE;
if (BORDER == Room::Border::BOTTOM) {
killPlayer();
}
}
}
}
// Finaliza la transición entre pantallas
void Game::endTransition() {
player_->setPaused(false);
room_->setPaused(false);
if (transition_old_room_) { transition_old_room_->setPaused(false); }
transitioning_ = false;
transition_just_ended_ = true;
transition_timer_ = 0.0F;
transition_old_room_.reset();
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 si el jugador está sobre una plataforma móvil y lo transporta
void Game::checkPlayerAndPlatforms() {
auto* platform = room_->checkPlayerOnPlatform(player_->getCollider(), player_->getVY());
if (platform != nullptr) {
player_->applyPlatformDisplacement(platform->getLastDX(), platform->getCollider().y);
}
}
// Comprueba las colisiones del jugador con los objetos
void Game::checkPlayerAndKeys() {
room_->keyCollision(player_->getCollider());
}
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(Defaults::Sound::Files::DEATH, Audio::Group::GAME);
// Transicionar al estado BLACK_SCREEN (el respawn ocurre en transitionToState)
transitionToState(State::BLACK_SCREEN);
}
// 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(Defaults::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 ? Defaults::Music::Files::GAME_TRACK : Defaults::Music::Files::TITLE_TRACK;
// 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]);
}
}
}
}