Merge branch 'feat/gamepad'
This commit is contained in:
commit
a2b01ba9d5
13 files changed
+459
-28
No files matched your search
@@ -59,6 +59,7 @@ set(APP_SOURCES
|
||||
source/core/input/key_config.cpp
|
||||
source/core/input/global_inputs.cpp
|
||||
source/core/input/input_state.cpp
|
||||
source/core/input/gamepad_manager.cpp
|
||||
source/core/rendering/screen.cpp
|
||||
source/core/rendering/canvas.cpp
|
||||
source/core/rendering/notifier.cpp
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
notification:
|
||||
test: "notificacio de prova"
|
||||
gamepad_connected: "connectat"
|
||||
gamepad_disconnected: "desconnectat"
|
||||
|
||||
input:
|
||||
zoom: "zoom"
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
notification:
|
||||
test: "test notification"
|
||||
gamepad_connected: "connected"
|
||||
gamepad_disconnected: "disconnected"
|
||||
|
||||
input:
|
||||
zoom: "zoom"
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
#include "core/input/gamepad_manager.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/rendering/notification_presets.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// A Emscripten, SDL 3.4+ no casa el GUID dels mandos web: Chrome/Android
|
||||
// no aporten Vendor/Product ID i el parser de SDL extreu dades brutes.
|
||||
// El W3C Gamepad API, però, garanteix un layout estàndard quan el mapping
|
||||
// reporta "standard", així que injectem un mapping SDL explícit amb eixe
|
||||
// layout per al GUID del joystick abans que SDL intenti obrir-lo com a
|
||||
// gamepad. Fora d'Emscripten és un no-op.
|
||||
void installWebStandardMapping(SDL_JoystickID jid) {
|
||||
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
|
||||
char guid_str[33];
|
||||
SDL_GUIDToString(guid, guid_str, sizeof(guid_str));
|
||||
const char* name = SDL_GetJoystickNameForID(jid);
|
||||
if (name == nullptr || *name == '\0') { name = "Standard Gamepad"; }
|
||||
|
||||
char mapping[512];
|
||||
SDL_snprintf(mapping, sizeof(mapping),
|
||||
"%s,%s,"
|
||||
"a:b0,b:b1,x:b2,y:b3,"
|
||||
"leftshoulder:b4,rightshoulder:b5,"
|
||||
"lefttrigger:b6,righttrigger:b7,"
|
||||
"back:b8,start:b9,"
|
||||
"leftstick:b10,rightstick:b11,"
|
||||
"dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
|
||||
"guide:b16,"
|
||||
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
|
||||
"platform:Emscripten",
|
||||
guid_str,
|
||||
name);
|
||||
SDL_AddGamepadMapping(mapping);
|
||||
}
|
||||
#else
|
||||
void installWebStandardMapping(SDL_JoystickID /*jid*/) {}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
auto GamepadManager::get() -> GamepadManager& {
|
||||
static GamepadManager instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
auto GamepadManager::prettyName(const char* raw) -> std::string {
|
||||
std::string name = (raw != nullptr && *raw != '\0') ? std::string(raw) : std::string("Gamepad");
|
||||
const auto POS = name.find_first_of("([");
|
||||
if (POS != std::string::npos) { name.erase(POS); }
|
||||
while (!name.empty() && name.back() == ' ') { name.pop_back(); }
|
||||
if (name.size() > 25) { name.resize(25); }
|
||||
if (name.empty()) { name = "Gamepad"; }
|
||||
return name;
|
||||
}
|
||||
|
||||
auto GamepadManager::uniqueName(const std::string& base) const -> std::string {
|
||||
const bool COLLISION = std::ranges::any_of(
|
||||
entries_,
|
||||
[&](const Entry& e) { return e.pretty_name == base; });
|
||||
if (!COLLISION) { return base; }
|
||||
|
||||
// Format " #N" començant a #2 — la primera ocurrència manté el nom net.
|
||||
for (int i = 2; i < 100; ++i) {
|
||||
std::string candidate = base + " #" + std::to_string(i);
|
||||
const bool MATCH = std::ranges::any_of(
|
||||
entries_,
|
||||
[&](const Entry& e) { return e.pretty_name == candidate; });
|
||||
if (!MATCH) { return candidate; }
|
||||
}
|
||||
return base; // cas ultra-improbable: 99 mandos idèntics connectats
|
||||
}
|
||||
|
||||
void GamepadManager::addGamepad(SDL_JoystickID jid, bool notify) {
|
||||
installWebStandardMapping(jid);
|
||||
if (!SDL_IsGamepad(jid)) { return; }
|
||||
SDL_Gamepad* pad = SDL_OpenGamepad(jid);
|
||||
if (pad == nullptr) {
|
||||
std::cout << "GamepadManager: SDL_OpenGamepad(" << jid
|
||||
<< ") ha fallat: " << SDL_GetError() << '\n';
|
||||
return;
|
||||
}
|
||||
std::string name = uniqueName(prettyName(SDL_GetGamepadName(pad)));
|
||||
std::cout << "GamepadManager: connectat " << name << '\n';
|
||||
entries_.push_back(Entry{.pad = pad, .instance_id = jid, .pretty_name = name});
|
||||
if (notify) {
|
||||
Notification::notify(name + ' ' + Locale::get().text("notification.gamepad_connected"));
|
||||
}
|
||||
}
|
||||
|
||||
void GamepadManager::removeGamepad(SDL_JoystickID jid) {
|
||||
auto it = std::ranges::find_if(
|
||||
entries_,
|
||||
[jid](const Entry& e) { return e.instance_id == jid; });
|
||||
if (it == entries_.end()) { return; }
|
||||
std::string name = std::move(it->pretty_name);
|
||||
if (it->pad != nullptr) { SDL_CloseGamepad(it->pad); }
|
||||
std::cout << "GamepadManager: desconnectat " << name << '\n';
|
||||
entries_.erase(it);
|
||||
Notification::notify(name + ' ' + Locale::get().text("notification.gamepad_disconnected"));
|
||||
}
|
||||
|
||||
void GamepadManager::init() {
|
||||
if (initialized_) { return; }
|
||||
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD)) {
|
||||
std::cout << "GamepadManager: SDL_INIT_GAMEPAD ha fallat: "
|
||||
<< SDL_GetError() << '\n';
|
||||
return;
|
||||
}
|
||||
// `gamecontrollerdb.txt` és opcional. Si no existeix, SDL retorna -1 i
|
||||
// continuem amb les definicions internes del sistema.
|
||||
const int ADDED = SDL_AddGamepadMappingsFromFile("data/input/gamecontrollerdb.txt");
|
||||
if (ADDED > 0) {
|
||||
std::cout << "GamepadManager: " << ADDED << " mappings de gamecontrollerdb.txt\n";
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
SDL_JoystickID* ids = SDL_GetJoysticks(&count);
|
||||
if (ids != nullptr) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
addGamepad(ids[i], /*notify=*/false);
|
||||
}
|
||||
SDL_free(ids);
|
||||
}
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
void GamepadManager::shutdown() {
|
||||
for (auto& entry : entries_) {
|
||||
if (entry.pad != nullptr) { SDL_CloseGamepad(entry.pad); }
|
||||
}
|
||||
entries_.clear();
|
||||
if (initialized_) {
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
|
||||
initialized_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void GamepadManager::handleEvent(const SDL_Event& ev) {
|
||||
if (ev.type == SDL_EVENT_GAMEPAD_ADDED) {
|
||||
addGamepad(ev.gdevice.which, /*notify=*/true);
|
||||
} else if (ev.type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
removeGamepad(ev.gdevice.which);
|
||||
}
|
||||
}
|
||||
|
||||
auto GamepadManager::getPrimary() const -> SDL_Gamepad* {
|
||||
return entries_.empty() ? nullptr : entries_.front().pad;
|
||||
}
|
||||
|
||||
auto GamepadManager::getCount() const -> std::size_t {
|
||||
return entries_.size();
|
||||
}
|
||||
|
||||
auto GamepadManager::getByPlayerIndex(std::size_t idx) const -> SDL_Gamepad* {
|
||||
return idx < entries_.size() ? entries_[idx].pad : nullptr;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Gestor global de gamepads. Descobreix els mandos presents a `init()`
|
||||
// sense emetre notificacions (el jugador ja els té posats al llançar el
|
||||
// joc). Durant el runtime, `handleEvent()` processa els events de hot-plug
|
||||
// i emet notificacions visibles via `Notification::notify`.
|
||||
//
|
||||
// El polling real de botons i eixos NO es fa a través d'aquesta classe:
|
||||
// els consumidors obtenen el `SDL_Gamepad*` primari (o l'enèsim per a
|
||||
// multi-jugador futur) i el passen a `InputState::getGamepadButton/Axis`,
|
||||
// que ho filtra pel flag de bloqueig de la UI (mateixa estratègia que
|
||||
// amb el teclat).
|
||||
class GamepadManager {
|
||||
public:
|
||||
static auto get() -> GamepadManager&;
|
||||
|
||||
GamepadManager(const GamepadManager&) = delete;
|
||||
GamepadManager(GamepadManager&&) = delete;
|
||||
auto operator=(const GamepadManager&) -> GamepadManager& = delete;
|
||||
auto operator=(GamepadManager&&) -> GamepadManager& = delete;
|
||||
|
||||
// Inicialitza el subsistema SDL de gamepad, intenta carregar un
|
||||
// `gamecontrollerdb.txt` opcional i obri silenciosament tots els
|
||||
// mandos ja connectats. Cridat una sola vegada des de Director.
|
||||
void init();
|
||||
|
||||
// Tanca tots els gamepads oberts i treu el subsistema SDL. Cridat
|
||||
// des de `~Director` abans de `SDL_Quit` per evitar handles orfes.
|
||||
void shutdown();
|
||||
|
||||
// Processa SDL_EVENT_GAMEPAD_ADDED i SDL_EVENT_GAMEPAD_REMOVED
|
||||
// emesos durant el runtime. Emet notificacions "<nom> connectat" o
|
||||
// "<nom> desconnectat". El Director decideix quins events li arriben
|
||||
// via `processGamepadEvents()`.
|
||||
void handleEvent(const SDL_Event& ev);
|
||||
|
||||
// Primary = primer gamepad al vector. `nullptr` si no n'hi ha cap.
|
||||
// Ús típic: Player 1 i navegació del ServiceMenu.
|
||||
[[nodiscard]] auto getPrimary() const -> SDL_Gamepad*;
|
||||
|
||||
[[nodiscard]] auto getCount() const -> std::size_t;
|
||||
|
||||
// Retorna el mand del jugador `idx`-èsim (0-indexed) o `nullptr` si
|
||||
// encara no hi ha prou mandos connectats. Reservat per a multi-jugador.
|
||||
[[nodiscard]] auto getByPlayerIndex(std::size_t idx) const -> SDL_Gamepad*;
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
SDL_Gamepad* pad{nullptr};
|
||||
SDL_JoystickID instance_id{0};
|
||||
std::string pretty_name;
|
||||
};
|
||||
|
||||
GamepadManager() = default;
|
||||
~GamepadManager() = default;
|
||||
|
||||
// Retalla `raw` al primer '(' o '[', elimina espais finals i talla
|
||||
// a 25 caràcters. Fallback "Gamepad" si queda buit.
|
||||
[[nodiscard]] static auto prettyName(const char* raw) -> std::string;
|
||||
|
||||
// Si `base` ja existeix al vector, afegeix " #2", " #3", ... fins que
|
||||
// trobi un nom lliure. Així dos mandos idèntics es distingeixen a les
|
||||
// notificacions.
|
||||
[[nodiscard]] auto uniqueName(const std::string& base) const -> std::string;
|
||||
|
||||
void addGamepad(SDL_JoystickID jid, bool notify);
|
||||
void removeGamepad(SDL_JoystickID jid);
|
||||
|
||||
std::vector<Entry> entries_;
|
||||
bool initialized_{false};
|
||||
};
|
||||
@@ -28,4 +28,12 @@ namespace InputState {
|
||||
return blocked ? zeroKeyboard() : SDL_GetKeyboardState(nullptr);
|
||||
}
|
||||
|
||||
auto getGamepadButton(SDL_Gamepad* gp, SDL_GamepadButton b) -> bool {
|
||||
return (blocked || gp == nullptr) ? false : SDL_GetGamepadButton(gp, b);
|
||||
}
|
||||
|
||||
auto getGamepadAxis(SDL_Gamepad* gp, SDL_GamepadAxis ax) -> Sint16 {
|
||||
return (blocked || gp == nullptr) ? static_cast<Sint16>(0) : SDL_GetGamepadAxis(gp, ax);
|
||||
}
|
||||
|
||||
} // namespace InputState
|
||||
@@ -14,17 +14,9 @@
|
||||
// que l'escena els vegi, així que la seva navegació segueix funcionant per
|
||||
// molt que el flag estigui posat.
|
||||
//
|
||||
// --- Nota per a l'extensió a gamepad (pròxima iteració) ---
|
||||
// Quan s'afegeixi suport de comandament, el jugador haurà de llegir eixos
|
||||
// i botons via polling (p.ex. `SDL_GetGamepadAxis`/`SDL_GetGamepadButton`).
|
||||
// Aquest mòdul s'ha d'ampliar amb:
|
||||
// [[nodiscard]] auto getGamepadAxis(SDL_Gamepad* gp, SDL_GamepadAxis ax)
|
||||
// -> Sint16; // 0 si bloquejat
|
||||
// [[nodiscard]] auto getGamepadButton(SDL_Gamepad* gp, SDL_GamepadButton b)
|
||||
// -> bool; // false si bloquejat
|
||||
// Mateix patró: zero/false quan `blocked_`, pass-through altrament. Així
|
||||
// Player (i el que sigui) només ha de canviar el nom de la crida; el menú
|
||||
// no ha de saber res del gamepad més enllà de processar els seus events.
|
||||
// El mateix patró aplica al gamepad: `getGamepadButton`/`getGamepadAxis`
|
||||
// retornen zero/false quan el bloqueig és actiu. Els mandos concrets els
|
||||
// obtén el caller via `GamepadManager::get().getPrimary()` o similars.
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
@@ -43,4 +35,12 @@ namespace InputState {
|
||||
// guardar-lo frame a frame.
|
||||
[[nodiscard]] auto getKeyboardState() -> const bool*;
|
||||
|
||||
// Versió filtrada de `SDL_GetGamepadButton`. Retorna false si el bloqueig
|
||||
// és actiu o si `gp` és nullptr (cas habitual: no hi ha cap mand).
|
||||
[[nodiscard]] auto getGamepadButton(SDL_Gamepad* gp, SDL_GamepadButton b) -> bool;
|
||||
|
||||
// Versió filtrada de `SDL_GetGamepadAxis`. Retorna 0 si el bloqueig
|
||||
// és actiu o si `gp` és nullptr.
|
||||
[[nodiscard]] auto getGamepadAxis(SDL_Gamepad* gp, SDL_GamepadAxis ax) -> Sint16;
|
||||
|
||||
} // namespace InputState
|
||||
@@ -1,11 +1,59 @@
|
||||
#include "core/rendering/notifier.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/rendering/text.hpp"
|
||||
#include "utils/defines.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Parteix `text` en una llista de línies que caben cadascuna dins
|
||||
// `max_width`. Algoritme de projecte_2026: tall per espais, la paraula
|
||||
// que no cap passa a la línia següent. Les paraules aïllades més
|
||||
// llargues que `max_width` es queden a la seva pròpia línia sense
|
||||
// trencar-les (cas improbable amb els pretty_names de gamepad retallats
|
||||
// a ~25 caràcters).
|
||||
auto wrapToWidth(const std::string& text, int max_width, const Text& font, int kerning) -> std::vector<std::string> {
|
||||
std::vector<std::string> lines;
|
||||
if (text.empty()) {
|
||||
lines.emplace_back();
|
||||
return lines;
|
||||
}
|
||||
|
||||
std::string current;
|
||||
std::size_t i = 0;
|
||||
while (i < text.size()) {
|
||||
const std::size_t SPACE_POS = text.find(' ', i);
|
||||
const bool LAST = (SPACE_POS == std::string::npos);
|
||||
const std::string WORD = text.substr(i, LAST ? std::string::npos : SPACE_POS - i);
|
||||
|
||||
if (current.empty()) {
|
||||
current = WORD;
|
||||
} else {
|
||||
std::string candidate = current;
|
||||
candidate += ' ';
|
||||
candidate += WORD;
|
||||
if (font.length(candidate, kerning) <= max_width) {
|
||||
current = std::move(candidate);
|
||||
} else {
|
||||
lines.push_back(current);
|
||||
current = WORD;
|
||||
}
|
||||
}
|
||||
|
||||
if (LAST) { break; }
|
||||
i = SPACE_POS + 1;
|
||||
}
|
||||
|
||||
lines.push_back(current);
|
||||
return lines;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto Notifier::get() -> Notifier& {
|
||||
static Notifier instance_;
|
||||
return instance_;
|
||||
@@ -34,8 +82,8 @@ void Notifier::update(int delta_ms) {
|
||||
break;
|
||||
case Status::VANISHING:
|
||||
y_ -= SLIDE_SPEED * DT;
|
||||
if (y_ <= static_cast<float>(-HEIGHT)) {
|
||||
y_ = static_cast<float>(-HEIGHT - MARGIN_TOP);
|
||||
if (y_ <= static_cast<float>(-current_box_h_)) {
|
||||
y_ = static_cast<float>(-current_box_h_ - MARGIN_TOP);
|
||||
status_ = Status::HIDDEN;
|
||||
}
|
||||
break;
|
||||
@@ -45,31 +93,45 @@ void Notifier::update(int delta_ms) {
|
||||
}
|
||||
|
||||
void Notifier::render() {
|
||||
if (status_ == Status::HIDDEN || text_ == nullptr || current_text_.empty()) { return; }
|
||||
if (status_ == Status::HIDDEN || text_ == nullptr || current_lines_.empty()) { return; }
|
||||
|
||||
const int TEXT_W = text_->length(current_text_, 1);
|
||||
const int BOX_W = TEXT_W + (2 * PADDING_H);
|
||||
const int BOX_X = (GameCanvas::WIDTH - BOX_W) / 2;
|
||||
const int BOX_X = (GameCanvas::WIDTH - current_box_w_) / 2;
|
||||
const int BOX_Y = static_cast<int>(y_);
|
||||
|
||||
// Fons sòlid amb la paleta UI que ens ha injectat el caller a init()
|
||||
// (ARGB directe al canvas, sense tocar cap surface paletada).
|
||||
Screen::get().fillRect(BOX_X, BOX_Y, BOX_W, HEIGHT, ui_palette_[current_bg_color_]);
|
||||
Screen::get().fillRect(BOX_X, BOX_Y, current_box_w_, current_box_h_, ui_palette_[current_bg_color_]);
|
||||
|
||||
const int LINE_STEP = Rendering::Hud::FONT_SIZE_PX + LINE_GAP;
|
||||
int line_y = BOX_Y + PADDING_V;
|
||||
for (const auto& line : current_lines_) {
|
||||
text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
|
||||
GameCanvas::CENTER_X,
|
||||
BOX_Y + PADDING_V,
|
||||
current_text_,
|
||||
line_y,
|
||||
line,
|
||||
1,
|
||||
current_text_color_);
|
||||
line_y += LINE_STEP;
|
||||
}
|
||||
}
|
||||
|
||||
void Notifier::notify(const std::string& text, Uint8 text_color, Uint8 bg_color, int time_ms) {
|
||||
// Política simple: la nova notificació reemplaça la que hi havia. Per
|
||||
// gestionar-ne una cua, canviar ací per una FIFO quan calga.
|
||||
current_text_ = text;
|
||||
if (text_ == nullptr) { return; }
|
||||
|
||||
current_lines_ = wrapToWidth(text, MAX_TEXT_WIDTH, *text_, 1);
|
||||
|
||||
// Amplada = max de les línies + padding. Alçada = N*glif + (N-1)*gap + padding.
|
||||
const int MAX_LINE_W = std::accumulate(current_lines_.begin(), current_lines_.end(), 0, [this](int acc, const std::string& s) { return std::max(acc, text_->length(s, 1)); });
|
||||
current_box_w_ = MAX_LINE_W + (2 * PADDING_H);
|
||||
|
||||
const int N = static_cast<int>(current_lines_.size());
|
||||
current_box_h_ = (N * Rendering::Hud::FONT_SIZE_PX) + ((N - 1) * LINE_GAP) + (2 * PADDING_V);
|
||||
|
||||
current_text_color_ = text_color;
|
||||
current_bg_color_ = bg_color;
|
||||
remaining_ms_ = time_ms;
|
||||
y_ = static_cast<float>(-current_box_h_ - MARGIN_TOP);
|
||||
status_ = Status::RISING;
|
||||
}
|
||||
@@ -5,9 +5,11 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/rendering/hud_constants.hpp"
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "utils/defines.hpp"
|
||||
|
||||
class Text;
|
||||
|
||||
@@ -19,6 +21,10 @@ class Text;
|
||||
// API crua: Notifier::get().notify(text, text_color, bg_color, time_ms).
|
||||
// Per a treballar amb presets pre-definits, vegeu Notification::notify() a
|
||||
// core/rendering/notification_presets.hpp.
|
||||
//
|
||||
// Multilinia: si el text passa de l'amplada màxima (`MAX_TEXT_WIDTH`), es
|
||||
// parteix per espais i la caixa creix en alçada automàticament. Els textos
|
||||
// curts (cas habitual) ocupen una sola línia i no canvien d'aspecte.
|
||||
class Notifier {
|
||||
public:
|
||||
static auto get() -> Notifier&;
|
||||
@@ -39,9 +45,13 @@ class Notifier {
|
||||
|
||||
static constexpr int PADDING_H = 6;
|
||||
static constexpr int PADDING_V = 3;
|
||||
static constexpr int HEIGHT = Rendering::Hud::FONT_SIZE_PX + (2 * PADDING_V);
|
||||
static constexpr int LINE_GAP = 1; // Separació vertical entre línies multilinia.
|
||||
static constexpr int MARGIN_TOP = 2; // Separació amb la vora superior del canvas.
|
||||
static constexpr float SLIDE_SPEED = 120.0F;
|
||||
// Amplada màxima (sense comptar padding) que pot ocupar el text abans que
|
||||
// `wrapToWidth` el parteixi en línies. Deixem marge respecte a la vora del
|
||||
// canvas perquè la caixa no toqui els laterals.
|
||||
static constexpr int MAX_TEXT_WIDTH = GameCanvas::WIDTH - 20 - (2 * PADDING_H);
|
||||
|
||||
private:
|
||||
enum class Status : std::uint8_t { HIDDEN,
|
||||
@@ -55,8 +65,10 @@ class Notifier {
|
||||
std::shared_ptr<Text> text_;
|
||||
Palette ui_palette_{};
|
||||
Status status_{Status::HIDDEN};
|
||||
float y_{static_cast<float>(-HEIGHT - MARGIN_TOP)};
|
||||
std::string current_text_;
|
||||
float y_{0.0F};
|
||||
std::vector<std::string> current_lines_;
|
||||
int current_box_w_{0};
|
||||
int current_box_h_{0};
|
||||
Uint8 current_text_color_{0};
|
||||
Uint8 current_bg_color_{0};
|
||||
int remaining_ms_{0};
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/input/gamepad_manager.hpp"
|
||||
#include "core/input/key_config.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/rendering/notifier.hpp"
|
||||
@@ -102,6 +103,11 @@ Director::Director() {
|
||||
// JA_Music_t que continga, així que no queden streams orfes.
|
||||
Audio::init(Options::buildAudioConfig());
|
||||
|
||||
// Gamepad: descobreix mandos presents silenciosament. A partir d'ara
|
||||
// els events SDL_EVENT_GAMEPAD_ADDED/REMOVED arribaran via
|
||||
// `processGamepadEvents()` i dispararan notificacions.
|
||||
GamepadManager::get().init();
|
||||
|
||||
// Font compartida pels overlays: construïm una sola instància de Text
|
||||
// i la injectem als consumidors (Notifier, RenderInfo) per a que cap
|
||||
// d'ells hagi de conèixer els noms dels recursos concrets.
|
||||
@@ -200,6 +206,7 @@ Director::~Director() {
|
||||
// desfà les llibreries abans que corren els destructors estàtics.
|
||||
current_scene_.reset();
|
||||
Resource::Cache::get().clearAudio();
|
||||
GamepadManager::get().shutdown();
|
||||
Audio::destroy();
|
||||
Screen::get().shutdown();
|
||||
SDL_Quit();
|
||||
@@ -235,6 +242,32 @@ void Director::processEvents() {
|
||||
if (QUIT_REQUESTED) { current_scene_.reset(); }
|
||||
}
|
||||
|
||||
void Director::processGamepadEvents() {
|
||||
// Dues responsabilitats: (1) passar ADDED/REMOVED al GamepadManager
|
||||
// perquè obri/tanqui mandos i emeti les notificacions hot-plug; (2)
|
||||
// tractar el botó BACK com a toggle del menú quan està tancat (el
|
||||
// paral·lel de F9 per al teclat). Quan el menú està obert, el BACK
|
||||
// passa pel pipeline del `ServiceMenu::handleEvent()` via
|
||||
// `processMenuInput()` i allà es converteix en `close()`.
|
||||
for (auto it = EventBuffer::events.begin(); it != EventBuffer::events.end();) {
|
||||
bool consumed = false;
|
||||
if (it->type == SDL_EVENT_GAMEPAD_ADDED || it->type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
GamepadManager::get().handleEvent(*it);
|
||||
consumed = true;
|
||||
} else if (it->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN &&
|
||||
it->gbutton.button == SDL_GAMEPAD_BUTTON_BACK &&
|
||||
!ServiceMenu::get().isOpen()) {
|
||||
ServiceMenu::get().toggle();
|
||||
consumed = true;
|
||||
}
|
||||
if (consumed) {
|
||||
it = EventBuffer::events.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Director::processMenuInput() {
|
||||
// Intercepta input abans que l'escena vegi els events. Si el menú està
|
||||
// obert, consumeix TOTS els KEY_DOWN perquè ni GlobalInputs (F1–F8,
|
||||
@@ -263,10 +296,12 @@ void Director::switchSceneIfRequested() {
|
||||
}
|
||||
|
||||
auto Director::iterate() -> SDL_AppResult {
|
||||
// Ordre important: primer el menú (pot consumir tecles i INJECTAR un
|
||||
// SDL_EVENT_QUIT des de l'acció "Eixir del joc"), després processEvents
|
||||
// perquè vegi el QUIT al mateix frame i tanqui l'escena — altrament el
|
||||
// QUIT injectat es perdria quan es netegi EventBuffer al final d'iterate.
|
||||
// Ordre important: primer els events de gamepad (hot-plug + BACK que
|
||||
// obre el menú quan està tancat), després el menú (pot consumir tecles
|
||||
// i INJECTAR un SDL_EVENT_QUIT des de l'acció "Eixir del joc"), i
|
||||
// finalment processEvents perquè vegi el QUIT al mateix frame — altrament
|
||||
// el QUIT injectat es perdria quan es netegi EventBuffer al final.
|
||||
processGamepadEvents();
|
||||
processMenuInput();
|
||||
processEvents();
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ class Director {
|
||||
|
||||
void createSystemFolder();
|
||||
void processEvents();
|
||||
static void processGamepadEvents();
|
||||
static void processMenuInput();
|
||||
void switchSceneIfRequested();
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
#include "core/input/gamepad_manager.hpp"
|
||||
#include "core/input/input_state.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
@@ -20,6 +21,29 @@ namespace {
|
||||
// Alias local per reduir soroll; la constant viu a Defaults::Player.
|
||||
constexpr float SPEED_PX_PER_S = Defaults::Player::SPEED_PX_PER_S;
|
||||
|
||||
// Dead-zone del stick analògic esquerre (rang Sint16: -32768..32767).
|
||||
// Converteix eix analògic en direcció digital; per sota, es considera
|
||||
// repòs i no contribueix a la velocitat.
|
||||
constexpr Sint16 STICK_DEADZONE = 12000;
|
||||
|
||||
// Acumula la contribució del primer gamepad (cruceta + stick esquerre)
|
||||
// sobre els acumuladors de velocitat. Extracció d'`update()` per a
|
||||
// mantenir la complexitat cognitiva sota llindar (threshold 25).
|
||||
void accumulateGamepadMovement(float& vx, float& vy) {
|
||||
SDL_Gamepad* pad = GamepadManager::get().getPrimary();
|
||||
if (pad == nullptr) { return; }
|
||||
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT)) { vx -= SPEED_PX_PER_S; }
|
||||
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT)) { vx += SPEED_PX_PER_S; }
|
||||
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_UP)) { vy -= SPEED_PX_PER_S; }
|
||||
if (InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_DOWN)) { vy += SPEED_PX_PER_S; }
|
||||
const Sint16 LX = InputState::getGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX);
|
||||
const Sint16 LY = InputState::getGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY);
|
||||
if (LX < -STICK_DEADZONE) { vx -= SPEED_PX_PER_S; }
|
||||
if (LX > STICK_DEADZONE) { vx += SPEED_PX_PER_S; }
|
||||
if (LY < -STICK_DEADZONE) { vy -= SPEED_PX_PER_S; }
|
||||
if (LY > STICK_DEADZONE) { vy += SPEED_PX_PER_S; }
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Player::loadFrom(const std::string& yaml_path) {
|
||||
@@ -70,6 +94,14 @@ void Player::update(float delta_time) {
|
||||
if (keys[controls_.up]) { vy -= SPEED_PX_PER_S; }
|
||||
if (keys[controls_.down]) { vy += SPEED_PX_PER_S; }
|
||||
|
||||
// Cruceta i stick esquerre del primer gamepad (si n'hi ha). També
|
||||
// respecten el bloqueig d'`InputState`, així que amb el menú obert
|
||||
// els botons tornen false/0 i no contribueixen a la velocitat.
|
||||
// Direccions oposades (teclat left + d-pad right) es cancel·len de
|
||||
// forma natural; premuda simultània en la mateixa direcció no duplica
|
||||
// la velocitat perquè cada branca només resta/suma una vegada.
|
||||
accumulateGamepadMovement(vx, vy);
|
||||
|
||||
setVelX(vx);
|
||||
setVelY(vy);
|
||||
|
||||
|
||||
@@ -426,6 +426,41 @@ void ServiceMenu::activateCurrent(int direction) {
|
||||
|
||||
auto ServiceMenu::handleEvent(const SDL_Event& ev) -> bool {
|
||||
if (!open_) { return false; }
|
||||
|
||||
// Botons del gamepad: cruceta per navegar, EAST/NORTH com a OK,
|
||||
// WEST/SOUTH com a BACK de pàgina, BACK tanca el menú sencer. Quan
|
||||
// el menú està obert consumim TOTS els BUTTON_DOWN perquè cap botó
|
||||
// arribi als consumidors de gameplay. Paral·lel al consum total de
|
||||
// KEY_DOWN més avall.
|
||||
if (ev.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||
switch (ev.gbutton.button) {
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_UP:
|
||||
moveCursor(-1);
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
|
||||
moveCursor(+1);
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
|
||||
activateCurrent(-1);
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
|
||||
case SDL_GAMEPAD_BUTTON_EAST:
|
||||
case SDL_GAMEPAD_BUTTON_NORTH:
|
||||
activateCurrent(+1);
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_WEST:
|
||||
case SDL_GAMEPAD_BUTTON_SOUTH:
|
||||
popPage();
|
||||
break;
|
||||
case SDL_GAMEPAD_BUTTON_BACK:
|
||||
close();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ev.type != SDL_EVENT_KEY_DOWN) { return false; }
|
||||
|
||||
const auto SC = ev.key.scancode;
|
||||
|
||||
Reference in new issue
Block a user