239 lines
8.1 KiB
C++
239 lines
8.1 KiB
C++
#include "core/input/define_buttons.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <utility>
|
||
|
||
#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<Text> title_font,
|
||
std::shared_ptr<Text> 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<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||
sequence_.push_back({"menu.items.jump", GamepadBindings::Action::JUMP, static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)});
|
||
sequence_.push_back({"menu.items.service", GamepadBindings::Action::SERVICE, static_cast<int>(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<int>(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<GamepadBindings::Action, int> 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());
|
||
}
|
||
}
|