From fddea0c4490f68170e7f21a2aa790f5bc9b598b7 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 1 May 2026 09:03:20 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat(menu):=20navegaci=C3=B3=20del=20men?= =?UTF-8?q?=C3=BA=20de=20servei=20amb=20sticks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/game/ui/service_menu.cpp | 62 +++++++++++++++++++++++++++++++++ source/game/ui/service_menu.hpp | 14 ++++++++ 2 files changed, 76 insertions(+) diff --git a/source/game/ui/service_menu.cpp b/source/game/ui/service_menu.cpp index 6dfefaa..0327906 100644 --- a/source/game/ui/service_menu.cpp +++ b/source/game/ui/service_menu.cpp @@ -467,6 +467,63 @@ void ServiceMenu::playAcceptSound() { Audio::get()->playSound(UiConfig::get().menuAcceptSound(), Audio::Group::INTERFACE); } +void ServiceMenu::handleGamepadAxisMotion(const SDL_GamepadAxisEvent& ev) { + // Llindars amb histèresi: cal travessar STICK_THRESHOLD per "entrar" a + // un extrem, però només es torna a la zona neutra quan baixa de + // STICK_RELEASE. Així evitem que petits tremolors al voltant del + // llindar facin spam d'events. Valors triats per donar la mateixa + // sensació que el stick del jugador (deadzone=12000) però una mica + // més estrictes per a la navegació discreta del menú. + constexpr Sint16 STICK_THRESHOLD = 16000; + constexpr Sint16 STICK_RELEASE = 8000; + + std::int8_t* zone = nullptr; + bool horizontal = false; + switch (ev.axis) { + case SDL_GAMEPAD_AXIS_LEFTX: + zone = &lx_zone_; + horizontal = true; + break; + case SDL_GAMEPAD_AXIS_LEFTY: + zone = &ly_zone_; + horizontal = false; + break; + case SDL_GAMEPAD_AXIS_RIGHTX: + zone = &rx_zone_; + horizontal = true; + break; + case SDL_GAMEPAD_AXIS_RIGHTY: + zone = &ry_zone_; + horizontal = false; + break; + default: + return; // Ignorem L2/R2 i la resta. + } + + // Càlcul de la zona nova: si el valor està més enllà del llindar fort, + // entra a l'extrem; si està dins de la zona morta, torna a 0; altrament + // manté la zona prèvia (banda d'histèresi). + std::int8_t new_zone = *zone; + if (ev.value > STICK_THRESHOLD) { + new_zone = +1; + } else if (ev.value < -STICK_THRESHOLD) { + new_zone = -1; + } else if (std::abs(ev.value) < STICK_RELEASE) { + new_zone = 0; + } + if (new_zone == *zone) { return; } + *zone = new_zone; + if (new_zone == 0) { return; } // Soltar el stick no dispara res. + + if (horizontal) { + activateCurrent(new_zone); + playAcceptSound(); + } else { + moveCursor(new_zone); + playSelectSound(); + } +} + void ServiceMenu::handleGamepadButtonDown(const SDL_GamepadButtonEvent& ev) { // El botó SERVICE (per defecte BACK; remapejable per dispositiu via // `GamepadBindings`) tanca el menú sencer. Cada mando pot tenir el @@ -570,6 +627,11 @@ auto ServiceMenu::handleEvent(const SDL_Event& ev) -> bool { return true; } + if (ev.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) { + handleGamepadAxisMotion(ev.gaxis); + return true; + } + if (ev.type != SDL_EVENT_KEY_DOWN) { return false; } const auto SC = ev.key.scancode; diff --git a/source/game/ui/service_menu.hpp b/source/game/ui/service_menu.hpp index 8bd0d22..acb2943 100644 --- a/source/game/ui/service_menu.hpp +++ b/source/game/ui/service_menu.hpp @@ -208,6 +208,12 @@ class ServiceMenu { // sota el llindar de clang-tidy. void handleGamepadButtonDown(const SDL_GamepadButtonEvent& ev); + // Navegació via sticks analògics. Treballa per edge-detection: cada + // eix recorda en quina "zona" està (negativa / neutra / positiva) i + // sols dispara una acció quan creua de neutra a un extrem, igual que + // un botó del DPAD. Aplicada tant al stick esquerre com al dret. + void handleGamepadAxisMotion(const SDL_GamepadAxisEvent& ev); + // Sons d'UI: SELECT acompanya navegació (amunt/avall) i back; ACCEPT // acompanya modificació d'opció (esquerra/dreta/retorn). Els noms de // recurs venen d'`UiConfig` i el preload el fa el Director. @@ -270,6 +276,14 @@ class ServiceMenu { float transition_progress_{0.0F}; // 0 → 1 durant la transició de pàgina. bool capturing_{false}; // True mentre esperem una tecla per a un KEY_BIND. + // Estat per eix del stick per a edge-detection: -1, 0, +1 segons la + // zona on viu el valor actual. Un canvi de zona dispara una acció + // (només quan entra a un extrem); reset a 0 quan torna al centre. + std::int8_t lx_zone_{0}; + std::int8_t ly_zone_{0}; + std::int8_t rx_zone_{0}; + std::int8_t ry_zone_{0}; + // Sub-mòdul de captura de botons del mando. Quan està actiu absorbeix // tots els events de mando (BUTTON_DOWN/UP, AXIS_MOTION) i, en acabar, // persisteix la nova configuració via `GamepadBindings`. Replica el From d26ecec04f0c9f68655dc6f742ea806301a27069 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 1 May 2026 09:07:22 +0200 Subject: [PATCH 2/3] =?UTF-8?q?feat(menu):=20acceptar=20amb=20ATTACK=20i?= =?UTF-8?q?=20cancel=C2=B7lar=20amb=20JUMP=20del=20mando?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/game/ui/service_menu.cpp | 43 ++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/source/game/ui/service_menu.cpp b/source/game/ui/service_menu.cpp index 0327906..36b08f0 100644 --- a/source/game/ui/service_menu.cpp +++ b/source/game/ui/service_menu.cpp @@ -445,14 +445,14 @@ void ServiceMenu::activateCurrent(int direction) { namespace { - // Resol el botó SERVICE configurat per al mando que ha emès aquest - // event. -1 si no troba cap match (mando desconegut o desconnectat - // mentrestant). - auto serviceButtonForEvent(SDL_JoystickID jid) -> int { + // Resol el botó configurat per a una acció concreta (SERVICE / ATTACK + // / JUMP) al mando que ha emès aquest event. -1 si no troba cap match + // (mando desconegut o desconnectat mentrestant). + auto actionButtonForEvent(SDL_JoystickID jid, GamepadBindings::Action action) -> int { for (const auto& info : GamepadManager::get().getAll()) { if (info.pad == nullptr) { continue; } if (SDL_GetJoystickID(SDL_GetGamepadJoystick(info.pad)) != jid) { continue; } - return GamepadBindings::get().getButton(info.name, info.path, GamepadBindings::Action::SERVICE); + return GamepadBindings::get().getButton(info.name, info.path, action); } return -1; } @@ -525,15 +525,31 @@ void ServiceMenu::handleGamepadAxisMotion(const SDL_GamepadAxisEvent& ev) { } void ServiceMenu::handleGamepadButtonDown(const SDL_GamepadButtonEvent& ev) { - // El botó SERVICE (per defecte BACK; remapejable per dispositiu via - // `GamepadBindings`) tanca el menú sencer. Cada mando pot tenir el - // seu propi mapeig — resolem-lo a partir del joystick instance ID. - const int SERVICE_BTN = serviceButtonForEvent(ev.which); - if (SERVICE_BTN >= 0 && static_cast(ev.button) == SERVICE_BTN) { + // Tots tres botons rellevants (SERVICE/ATTACK/JUMP) són remapejables per + // dispositiu via `GamepadBindings`. Resolem-los una sola vegada a partir + // del joystick instance ID i comparem; els defaults (WEST=ATTACK, + // SOUTH=JUMP, BACK=SERVICE) preserven el comportament històric. + const int SERVICE_BTN = actionButtonForEvent(ev.which, GamepadBindings::Action::SERVICE); + const int ACCEPT_BTN = actionButtonForEvent(ev.which, GamepadBindings::Action::ATTACK); + const int CANCEL_BTN = actionButtonForEvent(ev.which, GamepadBindings::Action::JUMP); + const int BTN = static_cast(ev.button); + + if (SERVICE_BTN >= 0 && BTN == SERVICE_BTN) { close(); playSelectSound(); return; } + if (ACCEPT_BTN >= 0 && BTN == ACCEPT_BTN) { + activateCurrent(+1); + playAcceptSound(); + return; + } + if (CANCEL_BTN >= 0 && BTN == CANCEL_BTN) { + popPage(); + playSelectSound(); + return; + } + switch (ev.button) { case SDL_GAMEPAD_BUTTON_DPAD_UP: moveCursor(-1); @@ -548,16 +564,9 @@ void ServiceMenu::handleGamepadButtonDown(const SDL_GamepadButtonEvent& ev) { playAcceptSound(); break; case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: - case SDL_GAMEPAD_BUTTON_EAST: - case SDL_GAMEPAD_BUTTON_NORTH: activateCurrent(+1); playAcceptSound(); break; - case SDL_GAMEPAD_BUTTON_WEST: - case SDL_GAMEPAD_BUTTON_SOUTH: - popPage(); - playSelectSound(); - break; default: break; } From de7233a012fce453e9fc7a1694e0d6fe31d2cf79 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 1 May 2026 10:24:13 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fix(player):=20facing=20del=20sprite=20des?= =?UTF-8?q?=20del=20despla=C3=A7ament=20real,=20no=20de=20l'input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/game/entities/player.cpp | 38 ++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index ec7c2d2..f1864f2 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -330,15 +330,18 @@ void Player::updateFacingAndTurn(float delta_time, float vx, float vy) { intermediate_cardinal_ = intermediateFor(INPUT_CARD); turning_remaining_ms_ = static_cast(turn_config.duration_ms); applyCardinal(intermediate_cardinal_); - } else if (vx != 0.0F) { - // Tria de facing: prevalença lateral, EAST/WEST segons el signe - // de vx; verticals purs van a NORTH/SOUTH. En aturar-se - // conservem `facing_` per a l'animació stand. - facing_ = (vx < 0.0F) ? Facing::WEST : Facing::EAST; } else if (vy < 0.0F) { + // Tria de facing amb prevalença vertical: si hi ha component + // vertical, el sprite mira NORTH/SOUTH encara que també hi + // haja horitzontal. Així una diagonal (down+right, up+left, + // etc.) mostra el sprite mirant amunt o avall, no de costat. + // Només quan el moviment és purament horitzontal anem a + // EAST/WEST. En aturar-se conservem `facing_` per a stand. facing_ = Facing::NORTH; } else if (vy > 0.0F) { facing_ = Facing::SOUTH; + } else if (vx != 0.0F) { + facing_ = (vx < 0.0F) ? Facing::WEST : Facing::EAST; } } @@ -1097,6 +1100,31 @@ void Player::applyMovement(float delta_time) { clampToPlayfield(); + // Refinem el `facing_` amb el desplaçament REAL del frame (post + // col·lisió/snap), no amb la velocitat d'entrada: si l'usuari prem + // diagonal en un pasillo, `tryMove` ja haurà anul·lat l'eix tapat + // per la paret, així que el sprite mira en la direcció realment + // efectuada (la d'input cru no ho sap). En AIRBORNE el facing es + // preserva des del despegue (vegeu el comentari de `integrateJump`), + // i durant un gir actiu el cardinal intermedi mana, així que els + // saltem. Si DX==DY==0 (parat o totalment bloquejat) preservem el + // facing previ per a l'animació stand. La prioritat vertical en + // diagonals lliures és intranscendent en pasillos (un dels dos eixos + // serà sempre 0); manté coherència amb la tria d'`updateFacingAndTurn`. + if (!AIRBORNE && turning_remaining_ms_ <= 0.0F) { + const float DX = getPosX() - FRAME_START_X; + const float DY = getPosY() - FRAME_START_Y; + if (DY < 0.0F) { + facing_ = Facing::NORTH; + } else if (DY > 0.0F) { + facing_ = Facing::SOUTH; + } else if (DX < 0.0F) { + facing_ = Facing::WEST; + } else if (DX > 0.0F) { + facing_ = Facing::EAST; + } + } + const bool MOVED = (getPosX() != FRAME_START_X) || (getPosY() != FRAME_START_Y); // `being_pushed_` força `stand_*`: el jugador es desplaça sense input, // animar-lo com a caminant faria creure que està caminant quan de