// IWYU pragma: no_include #include "game/entities/player.hpp" #include // Para max, min #include // Para ceil, abs #include "core/audio/audio.hpp" // Para Audio #include "core/input/input.hpp" // Para Input, InputAction #include "core/rendering/sprite/animated_sprite.hpp" // Para AnimatedSprite #include "core/resources/resource_cache.hpp" // Para Resource #include "game/defaults.hpp" // Para Defaults::Game::Player, Defaults::Sound::Files #include "game/gameplay/room.hpp" // Para Room #include "game/gameplay/tile_collider.hpp" // Para TileCollider #include "game/options.hpp" // Para Cheat, Options #include "utils/defines.hpp" // Para PlayArea, Collision #ifdef _DEBUG #include "core/system/debug.hpp" // Para Debug #endif // ============================================================================ // Constructor // ============================================================================ Player::Player(const Data& player) : room_(player.room) { initSprite(player.animations_path); applySpawnValues(player.spawn_data); placeSprite(); initSounds(); previous_state_ = state_; } // ============================================================================ // Render // ============================================================================ void Player::render() { sprite_->render(); } // ============================================================================ // Update — pipeline principal (6 fases) // ============================================================================ void Player::update(float delta_time) { if (is_paused_) { return; } // 1. Leer input handleInput(); // 2. Calcular velocidades updateVelocity(delta_time); if (state_ == State::ON_AIR) { applyGravity(delta_time); } // 3. Saltar o caer a través de plataformas/slopes handleJumpAndDrop(); // 4. Aplicar movimiento con colisión moveHorizontal(delta_time); moveVertical(delta_time); // 5. Detectar caída checkFalling(); // 6. Kill tiles auto [ktc, kox, koy] = getCollisionContext(); if (ktc.touchesKillTile(x_ + kox, y_ + koy, WIDTH, HEIGHT)) { markAsDead(); } // 7. Finalizar syncSpriteAndCollider(); animate(delta_time); border_ = handleBorders(); #ifdef _DEBUG Debug::get()->set("P.X", std::to_string(static_cast(x_))); Debug::get()->set("P.Y", std::to_string(static_cast(y_))); Debug::get()->set("P.LGP", std::to_string(last_grounded_position_)); switch (state_) { case State::ON_GROUND: Debug::get()->set("P.STATE", "ON_GROUND"); break; case State::ON_SLOPE: Debug::get()->set("P.STATE", "ON_SLOPE"); break; case State::ON_AIR: Debug::get()->set("P.STATE", "ON_AIR"); break; } #endif } // ============================================================================ // Fase 1: Input // ============================================================================ void Player::handleInput() { if (ignore_input_) { return; } if (Input::get()->checkAction(InputAction::LEFT)) { wanna_go_ = Direction::LEFT; } else if (Input::get()->checkAction(InputAction::RIGHT)) { wanna_go_ = Direction::RIGHT; } else { wanna_go_ = Direction::NONE; } const bool JUMP_PRESSED = Input::get()->checkAction(InputAction::JUMP); wanna_jump_ = JUMP_PRESSED && !jump_held_; jump_held_ = JUMP_PRESSED; const bool DOWN_PRESSED = Input::get()->checkAction(InputAction::DOWN); wanna_down_ = DOWN_PRESSED && !down_held_; down_held_ = DOWN_PRESSED; } // ============================================================================ // Fase 2: Velocidades // ============================================================================ void Player::updateVelocity(float delta_time) { float target = 0.0F; switch (wanna_go_) { case Direction::LEFT: target = -HORIZONTAL_VELOCITY; break; case Direction::RIGHT: target = HORIZONTAL_VELOCITY; break; default: target = 0.0F; break; } // Detectar cambio de dirección (solo en suelo — en el aire no se gira) if (state_ != State::ON_AIR && target != 0.0F) { Direction new_facing = (target > 0.0F) ? Direction::RIGHT : Direction::LEFT; if (new_facing != facing_) { facing_ = new_facing; turning_ = true; sprite_->resetAnimation(); } } // Orientación del sprite (solo en suelo, en el aire se mantiene la dirección del salto) if (state_ != State::ON_AIR) { if (target > 0.0F) { sprite_->setFlip(Flip::RIGHT); } else if (target < 0.0F) { sprite_->setFlip(Flip::LEFT); } } // Inercia: aire = gradual ambas direcciones, suelo = instantáneo arranque, gradual frenada const float STEP = HORIZONTAL_ACCEL * delta_time; if (state_ == State::ON_AIR) { // En el aire, permitir mantener velocidad boosteada si vamos en la misma dirección float air_target = target; if ((target > 0.0F && vx_ > target) || (target < 0.0F && vx_ < target)) { air_target = vx_; // No frenar el boost } if (vx_ < air_target) { vx_ = std::min(vx_ + STEP, air_target); } else if (vx_ > air_target) { vx_ = std::max(vx_ - STEP, air_target); } } else { if (target != 0.0F) { vx_ = target; } else if (vx_ > 0.0F) { vx_ = std::max(vx_ - STEP, 0.0F); } else if (vx_ < 0.0F) { vx_ = std::min(vx_ + STEP, 0.0F); } } } void Player::applyGravity(float delta_time) { const float GRAVITY = (vy_ < 0.0F && !jump_held_) ? GRAVITY_FORCE * LOW_JUMP_GRAVITY_MULT : GRAVITY_FORCE; vy_ += GRAVITY * delta_time; vy_ = std::min(vy_, MAX_VY); } // ============================================================================ // Fase 3: Saltar y drop-through // ============================================================================ void Player::handleJumpAndDrop() { if (state_ == State::ON_AIR) { return; } if (wanna_jump_) { startJump(); return; } // Drop-through: plataforma passable if (wanna_down_ && state_ == State::ON_GROUND) { auto [tc, ox, oy] = getCollisionContext(); float foot_y = (y_ + oy) + HEIGHT; int foot_row = static_cast(foot_y) / Tile::SIZE; int left_col = static_cast(x_ + ox) / Tile::SIZE; int right_col = static_cast((x_ + ox) + WIDTH - 1) / Tile::SIZE; for (int col = left_col; col <= right_col; ++col) { if (tc.getTileAt(col, foot_row) == TileCollider::Tile::PASSABLE) { y_ += 1.0F; vy_ = 0.0F; transitionToState(State::ON_AIR); return; } } } } void Player::startJump() { vy_ = JUMP_VELOCITY; vx_ *= JUMP_SPEED_BOOST; last_grounded_position_ = y_; Audio::get()->playSound(jump_sound_, Audio::Group::GAME); transitionToState(State::ON_AIR); } // ============================================================================ // Fase 4a: Movimiento horizontal // ============================================================================ void Player::moveHorizontal(float delta_time) { if (vx_ == 0.0F) { // Aunque no haya movimiento horizontal, si estamos en slope hay que seguirla // (por si la gravedad nos ha movido o algo) return; } auto [tc, ox, oy] = getCollisionContext(); float new_x = x_ + (vx_ * delta_time); // Colisión con paredes (room actual) if (vx_ < 0.0F) { float wall = tc.checkWallLeft(new_x + ox, y_ + oy, WIDTH, HEIGHT); if (wall != Collision::NONE) { new_x = wall - ox; } } else { float wall = tc.checkWallRight(new_x + ox, y_ + oy, WIDTH, HEIGHT); if (wall != Collision::NONE) { new_x = wall - WIDTH - ox; } } // Cross-room: comprobar muros en rooms adyacentes auto cross = getCrossRoomChecks(); checkCrossRoomWallH(new_x, cross); x_ = new_x; // Si estamos en una slope, ajustar Y para seguirla if (state_ == State::ON_SLOPE) { followSlope(); } // Si estamos en suelo plano, detectar entrada a slope if (state_ == State::ON_GROUND) { detectSlopeEntry(); } } // Ajusta Y del jugador para seguir la superficie de la slope mientras camina. // Si el pie sale del tile actual, busca el siguiente tile de slope en la fila // actual y la inferior (las slopes en escalera bajan una fila por tile). // Si no encuentra slope, llama a exitSlope(). void Player::followSlope() { auto [tc, ox, oy] = getCollisionContext(); // SLOPE_L (\): pie izquierdo. SLOPE_R (/): pie derecho. float foot_x = (slope_type_ == TileCollider::Tile::SLOPE_L) ? (x_ + ox) : (x_ + ox) + WIDTH - 1; // Calcular Y en la slope actual float surface_y = tc.getSlopeY(slope_tile_x_, slope_tile_y_, foot_x); y_ = surface_y - HEIGHT - oy; // Comprobar si hemos salido del tile actual int foot_tile_x = static_cast(foot_x) / Tile::SIZE; int foot_tile_y = static_cast((y_ + oy) + HEIGHT) / Tile::SIZE; if (foot_tile_x != slope_tile_x_ || foot_tile_y != slope_tile_y_) { // Buscar slope en el tile calculado y en el de abajo (la escalera de slopes // siempre tiene el siguiente tile una fila arriba o abajo) for (int row = foot_tile_y; row <= foot_tile_y + 1; ++row) { auto new_tile = tc.getTileAt(foot_tile_x, row); if (new_tile == TileCollider::Tile::SLOPE_L || new_tile == TileCollider::Tile::SLOPE_R) { slope_tile_x_ = foot_tile_x; slope_tile_y_ = row; slope_type_ = new_tile; surface_y = tc.getSlopeY(slope_tile_x_, slope_tile_y_, foot_x); y_ = surface_y - HEIGHT - oy; return; } } exitSlope(); } } // El jugador ha salido del tile de slope sin encontrar otra slope adyacente. // Comprueba si hay suelo debajo (foot_y y foot_y+1 para cubrir el boundary exacto // entre filas cuando se sale por el extremo inferior de la slope). // Si hay suelo, snapea al borde del tile. Si no, empieza a caer. void Player::exitSlope() { auto [tc, ox, oy] = getCollisionContext(); float foot_y = (y_ + oy) + HEIGHT; // Comprobar suelo en la fila actual y la siguiente (al salir por abajo de una slope, // los pies pueden estar en el último pixel de la fila, justo antes del suelo) for (int check = 0; check <= 1; ++check) { float check_y = foot_y + check; if (tc.hasGroundBelow(x_ + ox, check_y, WIDTH)) { int row = static_cast(check_y) / Tile::SIZE; y_ = static_cast(row * Tile::SIZE) - HEIGHT - oy; transitionToState(State::ON_GROUND); return; } } vy_ = 0.0F; transitionToState(State::ON_AIR); } // Detecta si el jugador ha pisado una slope caminando desde suelo plano. // Las slopes en escalera están una fila arriba del suelo, así que checkSlopeBelow // también mira la fila superior. void Player::detectSlopeEntry() { auto [tc, ox, oy] = getCollisionContext(); float foot_y = (y_ + oy) + HEIGHT; auto slope = tc.checkSlopeBelow(x_ + ox, foot_y, WIDTH); if (slope.on_slope) { y_ = slope.surface_y - HEIGHT - oy; slope_tile_x_ = slope.tile_x; slope_tile_y_ = slope.tile_y; slope_type_ = slope.type; transitionToState(State::ON_SLOPE); } } // ============================================================================ // Fase 4b: Movimiento vertical // ============================================================================ void Player::moveVertical(float delta_time) { if (state_ != State::ON_AIR) { return; } auto [tc, ox, oy] = getCollisionContext(); float displacement = vy_ * delta_time; float old_y = y_; if (vy_ < 0.0F) { // Subiendo: comprobar techo float new_y = y_ + displacement; float ceiling = tc.checkCeiling(x_ + ox, new_y + oy, WIDTH); if (ceiling != Collision::NONE) { y_ = ceiling - oy; vy_ = 0.0F; } else { y_ = new_y; } } else if (vy_ > 0.0F) { // Bajando: comprobar suelo float foot_y = (y_ + oy) + HEIGHT; float new_foot_y = foot_y + displacement; auto hit = tc.checkFloor(x_ + ox, foot_y, WIDTH, new_foot_y); if (hit.y != Collision::NONE) { y_ = hit.y - HEIGHT - oy; if (hit.type == TileCollider::Tile::SLOPE_L || hit.type == TileCollider::Tile::SLOPE_R) { slope_tile_x_ = hit.tile_x; slope_tile_y_ = hit.tile_y; slope_type_ = hit.type; transitionToState(State::ON_SLOPE); } else { transitionToState(State::ON_GROUND); } } else { y_ += displacement; #ifdef _DEBUG if (y_ > PlayArea::BOTTOM + 100) { y_ = PlayArea::TOP + 2; } #endif } } // Cross-room: comprobar suelo/techo en rooms adyacentes auto cross = getCrossRoomChecks(); checkCrossRoomFloor(old_y, cross); } // ============================================================================ // Fase 5: Detección de caída // ============================================================================ void Player::checkFalling() { if (state_ == State::ON_AIR) { return; } auto [tc, ox, oy] = getCollisionContext(); if (state_ == State::ON_SLOPE) { // Verificar que el tile de slope sigue existiendo auto tile = tc.getTileAt(slope_tile_x_, slope_tile_y_); if (tile != TileCollider::Tile::SLOPE_L && tile != TileCollider::Tile::SLOPE_R) { vy_ = 0.0F; transitionToState(State::ON_AIR); } return; } // ON_GROUND: si está sobre una plataforma móvil, no comprobar tiles if (on_platform_) { return; } // ON_GROUND: comprobar si sigue habiendo suelo float foot_y = (y_ + oy) + HEIGHT; if (!tc.hasGroundBelow(x_ + ox, foot_y, WIDTH)) { // Sticking: si no hay suelo pero hay slope debajo, snapear a ella // para transición suave suelo→slope (bajada de rampas sin caer) auto slope = tc.checkSlopeBelow(x_ + ox, foot_y, WIDTH); if (slope.on_slope) { y_ = slope.surface_y - HEIGHT - oy; slope_tile_x_ = slope.tile_x; slope_tile_y_ = slope.tile_y; slope_type_ = slope.type; transitionToState(State::ON_SLOPE); return; } // Cross-room: comprobar suelo en rooms adyacentes antes de declarar caída if (hasCrossRoomGround(getCrossRoomChecks())) { return; } vy_ = 0.0F; transitionToState(State::ON_AIR); } } // ============================================================================ // Gestión de estado // ============================================================================ void Player::transitionToState(State state) { previous_state_ = state_; state_ = state; switch (state) { case State::ON_GROUND: vy_ = 0; // Clamp vx al aterrizar (el salto puede dar un boost extra) if (vx_ > HORIZONTAL_VELOCITY) { vx_ = HORIZONTAL_VELOCITY; } if (vx_ < -HORIZONTAL_VELOCITY) { vx_ = -HORIZONTAL_VELOCITY; } if (previous_state_ == State::ON_AIR) { Audio::get()->playSound(land_sound_, Audio::Group::GAME); } break; case State::ON_SLOPE: vy_ = 0; if (vx_ > HORIZONTAL_VELOCITY) { vx_ = HORIZONTAL_VELOCITY; } if (vx_ < -HORIZONTAL_VELOCITY) { vx_ = -HORIZONTAL_VELOCITY; } if (previous_state_ == State::ON_AIR) { Audio::get()->playSound(land_sound_, Audio::Group::GAME); } break; case State::ON_AIR: last_grounded_position_ = static_cast(y_); break; } } // ============================================================================ // Bordes de pantalla // ============================================================================ auto Player::handleBorders() const -> Room::Border { const float CENTER_X = x_ + (WIDTH / 2.0F); const float CENTER_Y = y_ + (HEIGHT / 2.0F); if (CENTER_X < PlayArea::LEFT) { return Room::Border::LEFT; } if (CENTER_X > PlayArea::RIGHT) { return Room::Border::RIGHT; } if (CENTER_Y < PlayArea::TOP) { return Room::Border::TOP; } if (CENTER_Y > PlayArea::BOTTOM) { return Room::Border::BOTTOM; } return Room::Border::NONE; } void Player::setAdjacentRoom(std::shared_ptr room, Room::Border direction) { adjacent_room_ = std::move(room); adjacent_direction_ = direction; } void Player::clearAdjacentRoom() { adjacent_room_.reset(); adjacent_direction_ = Room::Border::NONE; } auto Player::getCollisionContext() const -> CollisionContext { if (!adjacent_room_) { return {room_->getTileCollider(), 0.0F, 0.0F}; } const float CENTER_X = x_ + (WIDTH / 2.0F); const float CENTER_Y = y_ + (HEIGHT / 2.0F); switch (adjacent_direction_) { case Room::Border::TOP: if (CENTER_Y < PlayArea::TOP) { return {adjacent_room_->getTileCollider(), 0.0F, static_cast(PlayArea::HEIGHT)}; } break; case Room::Border::BOTTOM: if (CENTER_Y > PlayArea::BOTTOM) { return {adjacent_room_->getTileCollider(), 0.0F, -static_cast(PlayArea::HEIGHT)}; } break; case Room::Border::LEFT: if (CENTER_X < PlayArea::LEFT) { return {adjacent_room_->getTileCollider(), static_cast(PlayArea::WIDTH), 0.0F}; } break; case Room::Border::RIGHT: if (CENTER_X > PlayArea::RIGHT) { return {adjacent_room_->getTileCollider(), -static_cast(PlayArea::WIDTH), 0.0F}; } break; default: break; } return {room_->getTileCollider(), 0.0F, 0.0F}; } void Player::switchBorders() { switch (border_) { case Room::Border::TOP: y_ += PlayArea::HEIGHT; last_grounded_position_ = static_cast(y_); break; case Room::Border::BOTTOM: y_ -= PlayArea::HEIGHT; last_grounded_position_ = static_cast(y_); break; case Room::Border::RIGHT: x_ -= PlayArea::WIDTH; break; case Room::Border::LEFT: x_ += PlayArea::WIDTH; break; default: break; } border_ = Room::Border::NONE; syncSpriteAndCollider(); } // ============================================================================ // Geometría y renderizado // ============================================================================ void Player::syncSpriteAndCollider() { placeSprite(); collider_box_ = getRect(); } // Cross-room collision: asigna una room adyacente por índice void Player::setBorderRoom(int index, std::shared_ptr room) { if (index >= 0 && index < BORDER_ROOM_COUNT) { border_rooms_[index] = std::move(room); } } // Cross-room collision: limpia todas las rooms adyacentes void Player::clearBorderRooms() { for (auto& r : border_rooms_) { r.reset(); } } // Cross-room: construye la lista de rooms adyacentes que solapan con la bbox del jugador auto Player::getCrossRoomChecks() const -> CrossRoomChecks { // Offsets por room: TOP, RIGHT, BOTTOM, LEFT, TR, BR, BL, TL static constexpr float PW = static_cast(PlayArea::WIDTH); static constexpr float PH = static_cast(PlayArea::HEIGHT); static constexpr struct { float ox; float oy; } OFFSETS[BORDER_ROOM_COUNT] = { {0, PH}, {-PW, 0}, {0, -PH}, {PW, 0}, {-PW, PH}, {-PW, -PH}, {PW, -PH}, {PW, PH} }; bool over_top = y_ < 0.0F; bool over_right = (x_ + WIDTH) > PlayArea::RIGHT; bool over_bottom = (y_ + HEIGHT) > PlayArea::BOTTOM; bool over_left = x_ < 0.0F; bool needed[BORDER_ROOM_COUNT] = { over_top, over_right, over_bottom, over_left, over_top && over_right, over_bottom && over_right, over_bottom && over_left, over_top && over_left }; CrossRoomChecks result; for (int i = 0; i < BORDER_ROOM_COUNT; ++i) { if (needed[i] && border_rooms_[i]) { result.entries[result.count++] = {&border_rooms_[i]->getTileCollider(), OFFSETS[i].ox, OFFSETS[i].oy}; } } return result; } // Cross-room: comprueba muros horizontales en rooms adyacentes void Player::checkCrossRoomWallH(float& new_x, const CrossRoomChecks& checks) const { for (int i = 0; i < checks.count; ++i) { const auto& [tc, ox, oy] = checks.entries[i]; if (vx_ < 0.0F) { float wall = tc->checkWallLeft(new_x + ox, y_ + oy, WIDTH, HEIGHT); if (wall != Collision::NONE) { float corrected = wall - ox; if (corrected > new_x) { new_x = corrected; } } } else if (vx_ > 0.0F) { float wall = tc->checkWallRight(new_x + ox, y_ + oy, WIDTH, HEIGHT); if (wall != Collision::NONE) { float corrected = wall - WIDTH - ox; if (corrected < new_x) { new_x = corrected; } } } } } // Cross-room: comprueba suelo/techo en rooms adyacentes (usa old_y para rango correcto de checkFloor) void Player::checkCrossRoomFloor(float old_y, const CrossRoomChecks& checks) { for (int i = 0; i < checks.count; ++i) { const auto& [tc, ox, oy] = checks.entries[i]; if (vy_ < 0.0F) { float ceiling = tc->checkCeiling(x_ + ox, y_ + oy, WIDTH); if (ceiling != Collision::NONE) { float corrected = ceiling - oy; if (corrected > y_) { y_ = corrected; vy_ = 0.0F; } } } else if (vy_ > 0.0F) { float old_foot = old_y + oy + HEIGHT; float new_foot = y_ + oy + HEIGHT; auto hit = tc->checkFloor(x_ + ox, old_foot, WIDTH, new_foot); if (hit.y != Collision::NONE) { float corrected = hit.y - HEIGHT - oy; if (corrected < y_) { y_ = corrected; vy_ = 0.0F; transitionToState(State::ON_GROUND); } } } } } // Cross-room: comprueba si hay suelo bajo el jugador en alguna room adyacente auto Player::hasCrossRoomGround(const CrossRoomChecks& checks) const -> bool { for (int i = 0; i < checks.count; ++i) { const auto& [tc, ox, oy] = checks.entries[i]; float foot = y_ + oy + HEIGHT; if (tc->hasGroundBelow(x_ + ox, foot, WIDTH)) { return true; } } return false; } // Aplica el desplazamiento de una plataforma móvil al jugador void Player::applyPlatformDisplacement(float dx, float surface_y) { y_ = surface_y - HEIGHT; // Snap vertical al top de la plataforma x_ += dx; // Desplazamiento horizontal vy_ = 0.0F; on_platform_ = true; if (state_ != State::ON_GROUND) { transitionToState(State::ON_GROUND); } syncSpriteAndCollider(); } void Player::placeSprite() { sprite_->setPos(x_, y_); } void Player::animate(float delta_time) { // NOLINT(readability-make-member-function-const) if (state_ == State::ON_AIR) { turning_ = false; const bool NEAR_PEAK = vy_ > JUMP_VELOCITY * 0.5F && vy_ < -JUMP_VELOCITY * 0.5F; sprite_->setCurrentAnimation(NEAR_PEAK ? "jump_peak" : "jump"); } else if (vx_ != 0) { if (turning_) { sprite_->setCurrentAnimation("turn_walk"); } else { sprite_->setCurrentAnimation("walk"); } sprite_->update(delta_time); } else { turning_ = false; sprite_->setCurrentAnimation("stand"); } } // ============================================================================ // Skin // ============================================================================ auto Player::skinToAnimationPath(const std::string& skin_name) -> std::string { if (skin_name == Defaults::Game::Player::SKIN) { return Defaults::Game::Player::DEFAULT_ANIMATION; } return skin_name + ".yaml"; } // ============================================================================ // Inicialización // ============================================================================ void Player::initSprite(const std::string& animations_path) { // NOLINT(readability-convert-member-functions-to-static) const auto& animation_data = Resource::Cache::get()->getAnimationData(animations_path); sprite_ = std::make_unique(animation_data); sprite_->setWidth(WIDTH); sprite_->setHeight(HEIGHT); sprite_->setCurrentAnimation("walk"); } void Player::initSounds() { // NOLINT(readability-convert-member-functions-to-static) jump_sound_ = Resource::Cache::get()->getSound(Defaults::Sound::Files::JUMP); land_sound_ = Resource::Cache::get()->getSound(Defaults::Sound::Files::LAND); } void Player::applySpawnValues(const SpawnData& spawn) { x_ = spawn.x; y_ = spawn.y; vx_ = spawn.vx; vy_ = spawn.vy; last_grounded_position_ = spawn.last_grounded_position; state_ = spawn.state; sprite_->setFlip(spawn.flip); facing_ = (spawn.flip == Flip::LEFT) ? Direction::LEFT : Direction::RIGHT; } // ============================================================================ // Utilidades // ============================================================================ void Player::markAsDead() { is_alive_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED); } #ifdef _DEBUG void Player::setDebugPosition(float x, float y) { x_ = x; y_ = y; syncSpriteAndCollider(); } void Player::finalizeDebugTeleport() { vx_ = 0.0F; vy_ = 0.0F; last_grounded_position_ = static_cast(y_); slope_tile_x_ = 0; slope_tile_y_ = 0; slope_type_ = TileCollider::Tile::EMPTY; transitionToState(State::ON_GROUND); syncSpriteAndCollider(); } #endif