#include "core/input/define_buttons.hpp" #include #include #include "core/locale/locale.hpp" #include "core/rendering/screen.hpp" #include "core/rendering/text.hpp" #include "core/ui/ui_config.hpp" #include "utils/defines.hpp" namespace { // Threshold per als triggers L2/R2 — equivalent a CC (16384 ≈ 50%). constexpr Sint16 TRIGGER_THRESHOLD = 16384; // Geometria de l'overlay (canvas lògic 320×240). constexpr int BOX_WIDTH = 240; constexpr int PAD_Y = 8; constexpr int LINE_GAP = 4; constexpr Uint8 BG_ALPHA = 230; } // namespace void DefineButtons::init(std::shared_ptr title_font, std::shared_ptr items_font, const Palette& ui_palette) { title_font_ = std::move(title_font); items_font_ = std::move(items_font); ui_palette_ = ui_palette; } auto DefineButtons::enable(SDL_Gamepad* pad, std::string name, std::string path) -> bool { if (pad == nullptr) { return false; } pad_ = pad; pad_name_ = std::move(name); pad_path_ = std::move(path); enabled_ = true; finished_ = false; closing_ = false; message_shown_ = false; message_timer_ms_ = 0; l2_was_pressed_ = false; r2_was_pressed_ = false; resetSequence(); return true; } void DefineButtons::disable() { enabled_ = false; finished_ = false; closing_ = false; message_shown_ = false; message_timer_ms_ = 0; l2_was_pressed_ = false; r2_was_pressed_ = false; pad_ = nullptr; pad_name_.clear(); pad_path_.clear(); sequence_.clear(); index_ = 0; } void DefineButtons::resetSequence() { // Ordre: ATTACK, JUMP, SERVICE — replica l'estructura de // `clearButtons()` de coffee_crisis_arcade_edition (seqüència ordenada // d'accions amb la SERVICE al final) adaptada al joc actual. La label // de SERVICE va a una clau pròpia (`menu.items.service`) per no // reutilitzar la del submenú dels mandos. sequence_.clear(); sequence_.push_back({"menu.items.attack", GamepadBindings::Action::ATTACK, static_cast(SDL_GAMEPAD_BUTTON_INVALID)}); sequence_.push_back({"menu.items.jump", GamepadBindings::Action::JUMP, static_cast(SDL_GAMEPAD_BUTTON_INVALID)}); sequence_.push_back({"menu.items.service", GamepadBindings::Action::SERVICE, static_cast(SDL_GAMEPAD_BUTTON_INVALID)}); index_ = 0; } auto DefineButtons::isInUse(int button) const -> bool { return std::ranges::any_of(sequence_, [button](const ButtonAssignment& a) { return a.button == button; }); } auto DefineButtons::isReadyToClose() const -> bool { // Llest per tancar quan: la seqüència ha acabat, ja s'ha mostrat el // missatge, està en mode tancament, i el timer ha expirat. Replica el // patró de `coffee_crisis_arcade_edition::DefineButtons::isReadyToClose`, // simplificat perquè ací no tenim animació de WindowMessage. return finished_ && message_shown_ && closing_ && message_timer_ms_ <= 0; } void DefineButtons::update(int delta_ms) { if (!enabled_) { return; } if (finished_ && message_shown_) { message_timer_ms_ -= delta_ms; if (message_timer_ms_ <= 0 && !closing_) { closing_ = true; message_timer_ms_ = 0; // anchor a 0 per a `isReadyToClose`. } } } void DefineButtons::handleEvent(const SDL_Event& ev) { if (!enabled_) { return; } switch (ev.type) { case SDL_EVENT_GAMEPAD_BUTTON_DOWN: handleButtonDown(ev.gbutton); break; case SDL_EVENT_GAMEPAD_BUTTON_UP: onButtonUp(); break; case SDL_EVENT_GAMEPAD_AXIS_MOTION: handleAxisMotion(ev.gaxis); break; default: break; } } void DefineButtons::assignAndAdvance(int button) { if (finished_ || isInUse(button)) { return; } sequence_.at(index_).button = button; if (index_ + 1 < sequence_.size()) { ++index_; } else { finished_ = true; } } void DefineButtons::processTrigger(int virtual_button, bool& was_pressed, bool now) { if (now && !was_pressed) { assignAndAdvance(virtual_button); } if (!now && was_pressed) { onButtonUp(); } was_pressed = now; } void DefineButtons::handleButtonDown(const SDL_GamepadButtonEvent& ev) { // Filtra: només acceptem events del mando que estem configurant. // (Per l'instance_id no podem comparar directament; obtenim el handle // i el comparem.) const SDL_Gamepad* source = SDL_GetGamepadFromID(ev.which); if (source != pad_) { return; } assignAndAdvance(static_cast(ev.button)); } void DefineButtons::handleAxisMotion(const SDL_GamepadAxisEvent& ev) { const SDL_Gamepad* source = SDL_GetGamepadFromID(ev.which); if (source != pad_) { return; } // Triggers L2/R2 com a "botons" amb edge-detect (transició inactiu → // actiu). Replica `doControllerAxisMotion` de // coffee_crisis_arcade_edition. const bool NOW = ev.value > TRIGGER_THRESHOLD; if (ev.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) { processTrigger(GamepadBindings::TRIGGER_L2_AS_BUTTON, l2_was_pressed_, NOW); } else if (ev.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) { processTrigger(GamepadBindings::TRIGGER_R2_AS_BUTTON, r2_was_pressed_, NOW); } } void DefineButtons::onButtonUp() { if (!finished_ || message_shown_) { return; } persistAndShowComplete(); } void DefineButtons::persistAndShowComplete() { std::unordered_map mapping; for (const auto& a : sequence_) { mapping[a.action] = a.button; } GamepadBindings::get().setBindings(pad_name_, pad_path_, mapping); message_shown_ = true; message_timer_ms_ = MESSAGE_DISPLAY_DURATION_MS; } void DefineButtons::render() const { if (!enabled_ || !title_font_ || !items_font_) { return; } const int TITLE_H = title_font_->getLineHeight(); const int ITEM_H = items_font_->getLineHeight(); const int TITLE_KERNING = title_font_->getDefaultKerning(); const int ITEM_KERNING = items_font_->getDefaultKerning(); // Calcular l'alçada total: títol + 2 línies (instrucció + label). int box_h = (PAD_Y * 2) + TITLE_H + LINE_GAP + (ITEM_H * 2) + LINE_GAP; int box_x = (GameCanvas::WIDTH - BOX_WIDTH) / 2; int box_y = (GameCanvas::HEIGHT - box_h) / 2; const auto BG_ARGB = ui_palette_[UiConfig::get().menuBg()]; const auto BORDER_ARGB = ui_palette_[UiConfig::get().menuBorder()]; Screen::get().blendRect(box_x, box_y, BOX_WIDTH, box_h, BG_ARGB, BG_ALPHA); Screen::get().fillRect(box_x, box_y, BOX_WIDTH, 1, BORDER_ARGB); Screen::get().fillRect(box_x, box_y + box_h - 1, BOX_WIDTH, 1, BORDER_ARGB); Screen::get().fillRect(box_x, box_y, 1, box_h, BORDER_ARGB); Screen::get().fillRect(box_x + BOX_WIDTH - 1, box_y, 1, box_h, BORDER_ARGB); int cursor_y = box_y + PAD_Y; const int CENTER_X = box_x + (BOX_WIDTH / 2); // Títol: nom del mando. const std::string TITLE = pad_name_; title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, CENTER_X, cursor_y, TITLE, TITLE_KERNING, UiConfig::get().menuTitle()); cursor_y += TITLE_H + LINE_GAP; if (message_shown_) { // Pantalla final: missatge de configuració completa. items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, CENTER_X, cursor_y, Locale::get().text("menu.values.define_buttons_complete"), ITEM_KERNING, UiConfig::get().menuValue()); return; } // Instrucció + acció pendent. Replica visualment el WindowMessage de // coffee_crisis_arcade_edition. items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, CENTER_X, cursor_y, Locale::get().text("menu.values.press_button"), ITEM_KERNING, UiConfig::get().menuLabel()); cursor_y += ITEM_H + LINE_GAP; if (index_ < sequence_.size()) { const auto& current = sequence_[index_]; items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, CENTER_X, cursor_y, Locale::get().text(current.label_key), ITEM_KERNING, UiConfig::get().menuCursor()); } }