refactor: extreure helpers per reduir complexitat cognitiva (tidy net)
This commit is contained in:
@@ -195,97 +195,86 @@ namespace Gamepad {
|
||||
SDL_PushEvent(&e);
|
||||
}
|
||||
|
||||
// Estat agregat d'un frame: D-pad i stick combinats, més botons frontals.
|
||||
struct PadState {
|
||||
bool up;
|
||||
bool down;
|
||||
bool left;
|
||||
bool right;
|
||||
bool south;
|
||||
bool east;
|
||||
bool west;
|
||||
bool north;
|
||||
bool start;
|
||||
bool back;
|
||||
};
|
||||
|
||||
static auto readPadState() -> PadState {
|
||||
const Sint16 LX = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX);
|
||||
const Sint16 LY = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY);
|
||||
return PadState{
|
||||
.up = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_UP) || LY < -STICK_DEADZONE,
|
||||
.down = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_DOWN) || LY > STICK_DEADZONE,
|
||||
.left = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT) || LX < -STICK_DEADZONE,
|
||||
.right = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT) || LX > STICK_DEADZONE,
|
||||
.south = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_SOUTH),
|
||||
.east = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_EAST),
|
||||
.west = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_WEST),
|
||||
.north = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_NORTH),
|
||||
.start = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_START),
|
||||
.back = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_BACK),
|
||||
};
|
||||
}
|
||||
|
||||
static void handleMenuNavigation(const PadState& s) {
|
||||
if (s.up && !prev_up) { pushKey(SDL_SCANCODE_UP); }
|
||||
if (s.down && !prev_down) { pushKey(SDL_SCANCODE_DOWN); }
|
||||
if (s.left && !prev_left) { pushKey(SDL_SCANCODE_LEFT); }
|
||||
if (s.right && !prev_right) { pushKey(SDL_SCANCODE_RIGHT); }
|
||||
if (s.east && !prev_east) { pushKey(SDL_SCANCODE_RETURN); }
|
||||
if (s.south && !prev_south) { pushKey(SDL_SCANCODE_BACKSPACE); }
|
||||
// Mentre el menú està obert, el joc no ha de rebre moviment.
|
||||
Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, false);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, false);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, false);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, false);
|
||||
}
|
||||
|
||||
static void handleGameInput(const PadState& s) {
|
||||
Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, s.up);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, s.down);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, s.left);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, s.right);
|
||||
const bool ANY_FRONT_EDGE = (s.south && !prev_south) || (s.east && !prev_east) ||
|
||||
(s.west && !prev_west) || (s.north && !prev_north);
|
||||
if (ANY_FRONT_EDGE) {
|
||||
pushKey(SDL_SCANCODE_RETURN);
|
||||
}
|
||||
}
|
||||
|
||||
void update() {
|
||||
if (pad == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// D-pad
|
||||
bool dup = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
||||
bool ddn = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
||||
bool dlt = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
||||
bool drt = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
|
||||
|
||||
// Stick esquerre amb dead-zone
|
||||
Sint16 lx = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX);
|
||||
Sint16 ly = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY);
|
||||
bool sup = ly < -STICK_DEADZONE;
|
||||
bool sdn = ly > STICK_DEADZONE;
|
||||
bool slt = lx < -STICK_DEADZONE;
|
||||
bool srt = lx > STICK_DEADZONE;
|
||||
|
||||
bool up = dup || sup;
|
||||
bool dn = ddn || sdn;
|
||||
bool lt = dlt || slt;
|
||||
bool rt = drt || srt;
|
||||
|
||||
// Botons frontals (layout SDL: SOUTH=A/Cross, EAST=B/Circle, WEST=X/Square, NORTH=Y/Triangle)
|
||||
bool south = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_SOUTH);
|
||||
bool east = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_EAST);
|
||||
bool west = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_WEST);
|
||||
bool north = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_NORTH);
|
||||
bool start = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_START);
|
||||
bool back = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_BACK);
|
||||
|
||||
// Select (Back) → obre/tanca menú de servei (flanc)
|
||||
if (back && !prev_back) {
|
||||
pushKey(KeyConfig::scancode("menu_toggle"));
|
||||
}
|
||||
// Start → pausa (flanc)
|
||||
if (start && !prev_start) {
|
||||
pushKey(KeyConfig::scancode("pause_toggle"));
|
||||
}
|
||||
|
||||
const PadState S = readPadState();
|
||||
// Flancs globals: Select i Start sempre operen.
|
||||
if (S.back && !prev_back) { pushKey(KeyConfig::scancode("menu_toggle")); }
|
||||
if (S.start && !prev_start) { pushKey(KeyConfig::scancode("pause_toggle")); }
|
||||
if (Menu::isOpen()) {
|
||||
// Navegació del menú per flanc
|
||||
if (up && !prev_up) {
|
||||
pushKey(SDL_SCANCODE_UP);
|
||||
}
|
||||
if (dn && !prev_down) {
|
||||
pushKey(SDL_SCANCODE_DOWN);
|
||||
}
|
||||
if (lt && !prev_left) {
|
||||
pushKey(SDL_SCANCODE_LEFT);
|
||||
}
|
||||
if (rt && !prev_right) {
|
||||
pushKey(SDL_SCANCODE_RIGHT);
|
||||
}
|
||||
// EAST accepta, SOUTH cancela / endarrere
|
||||
if (east && !prev_east) {
|
||||
pushKey(SDL_SCANCODE_RETURN);
|
||||
}
|
||||
if (south && !prev_south) {
|
||||
pushKey(SDL_SCANCODE_BACKSPACE);
|
||||
}
|
||||
|
||||
// Assegura que el joc no rep tecles de moviment mentre el menú està obert
|
||||
Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, false);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, false);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, false);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, false);
|
||||
handleMenuNavigation(S);
|
||||
} else {
|
||||
// Moviment al joc — level-triggered (polling)
|
||||
Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, up);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, dn);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, lt);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, rt);
|
||||
// Qualsevol dels 4 botons frontals avança escenes (Ji::anyKey via Enter sintètic)
|
||||
if ((south && !prev_south) || (east && !prev_east) ||
|
||||
(west && !prev_west) || (north && !prev_north)) {
|
||||
pushKey(SDL_SCANCODE_RETURN);
|
||||
}
|
||||
handleGameInput(S);
|
||||
}
|
||||
|
||||
prev_up = up;
|
||||
prev_down = dn;
|
||||
prev_left = lt;
|
||||
prev_right = rt;
|
||||
prev_south = south;
|
||||
prev_east = east;
|
||||
prev_west = west;
|
||||
prev_north = north;
|
||||
prev_start = start;
|
||||
prev_back = back;
|
||||
prev_up = S.up;
|
||||
prev_down = S.down;
|
||||
prev_left = S.left;
|
||||
prev_right = S.right;
|
||||
prev_south = S.south;
|
||||
prev_east = S.east;
|
||||
prev_west = S.west;
|
||||
prev_north = S.north;
|
||||
prev_start = S.start;
|
||||
prev_back = S.back;
|
||||
}
|
||||
|
||||
} // namespace Gamepad
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "core/input/global_inputs.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "core/input/key_config.hpp"
|
||||
@@ -23,131 +24,71 @@ namespace GlobalInputs {
|
||||
static bool texture_filter_prev = false;
|
||||
static bool render_info_prev = false;
|
||||
|
||||
// Patró comú: lectura amb detecció de flanc + acumulació al flag "consumed".
|
||||
// `on_press` només s'executa al flanc puja; `prev` es manté actualitzat.
|
||||
static auto edgeTrigger(const char* key_id, bool& prev, const std::function<void()>& on_press) -> bool {
|
||||
const bool PRESSED = Ji::keyPressed(KeyConfig::scancode(key_id));
|
||||
if (PRESSED && !prev) {
|
||||
on_press();
|
||||
}
|
||||
prev = PRESSED;
|
||||
return PRESSED;
|
||||
}
|
||||
|
||||
auto handle() -> bool {
|
||||
bool consumed = false;
|
||||
|
||||
// F1 — Reduir zoom
|
||||
bool dec_zoom = Ji::keyPressed(KeyConfig::scancode("dec_zoom"));
|
||||
if (dec_zoom && !dec_zoom_prev) {
|
||||
consumed |= edgeTrigger("dec_zoom", dec_zoom_prev, [] {
|
||||
Screen::get()->decZoom();
|
||||
char msg[32];
|
||||
snprintf(msg, sizeof(msg), Locale::get("notifications.zoom_fmt"), Screen::get()->getZoom());
|
||||
Overlay::showNotification(msg);
|
||||
}
|
||||
if (dec_zoom) {
|
||||
consumed = true;
|
||||
}
|
||||
dec_zoom_prev = dec_zoom;
|
||||
|
||||
// F2 — Augmentar zoom
|
||||
bool inc_zoom = Ji::keyPressed(KeyConfig::scancode("inc_zoom"));
|
||||
if (inc_zoom && !inc_zoom_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("inc_zoom", inc_zoom_prev, [] {
|
||||
Screen::get()->incZoom();
|
||||
char msg[32];
|
||||
snprintf(msg, sizeof(msg), Locale::get("notifications.zoom_fmt"), Screen::get()->getZoom());
|
||||
Overlay::showNotification(msg);
|
||||
}
|
||||
if (inc_zoom) {
|
||||
consumed = true;
|
||||
}
|
||||
inc_zoom_prev = inc_zoom;
|
||||
|
||||
// F3 — Toggle pantalla completa
|
||||
bool fullscreen = Ji::keyPressed(KeyConfig::scancode("fullscreen"));
|
||||
if (fullscreen && !fullscreen_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("fullscreen", fullscreen_prev, [] {
|
||||
Screen::get()->toggleFullscreen();
|
||||
Overlay::showNotification(Screen::get()->isFullscreen() ? Locale::get("notifications.fullscreen") : Locale::get("notifications.windowed"));
|
||||
}
|
||||
if (fullscreen) {
|
||||
consumed = true;
|
||||
}
|
||||
fullscreen_prev = fullscreen;
|
||||
|
||||
// F4 — Toggle shaders
|
||||
bool shader = Ji::keyPressed(KeyConfig::scancode("toggle_shader"));
|
||||
if (shader && !shader_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("toggle_shader", shader_prev, [] {
|
||||
Screen::get()->toggleShaders();
|
||||
Overlay::showNotification(Options::video.shader_enabled ? Locale::get("notifications.shader_on") : Locale::get("notifications.shader_off"));
|
||||
}
|
||||
if (shader) {
|
||||
consumed = true;
|
||||
}
|
||||
shader_prev = shader;
|
||||
|
||||
// F5 — Toggle aspect ratio 4:3
|
||||
bool aspect = Ji::keyPressed(KeyConfig::scancode("toggle_aspect_ratio"));
|
||||
if (aspect && !aspect_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("toggle_aspect_ratio", aspect_prev, [] {
|
||||
Screen::get()->toggleAspectRatio();
|
||||
Overlay::showNotification(Options::video.aspect_ratio_4_3 ? Locale::get("notifications.aspect_43") : Locale::get("notifications.aspect_square"));
|
||||
}
|
||||
if (aspect) {
|
||||
consumed = true;
|
||||
}
|
||||
aspect_prev = aspect;
|
||||
|
||||
// F6 — Toggle supersampling
|
||||
bool ss = Ji::keyPressed(KeyConfig::scancode("toggle_supersampling"));
|
||||
if (ss && !ss_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("toggle_supersampling", ss_prev, [] {
|
||||
if (Screen::get()->toggleSupersampling()) {
|
||||
Overlay::showNotification(Options::video.supersampling ? Locale::get("notifications.ss_on") : Locale::get("notifications.ss_off"));
|
||||
}
|
||||
}
|
||||
if (ss) {
|
||||
consumed = true;
|
||||
}
|
||||
ss_prev = ss;
|
||||
|
||||
// F7 — Canviar tipus de shader (PostFX ↔ CrtPi)
|
||||
bool next_shader = Ji::keyPressed(KeyConfig::scancode("next_shader"));
|
||||
if (next_shader && !next_shader_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("next_shader", next_shader_prev, [] {
|
||||
if (Screen::get()->nextShaderType()) {
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg), "%s: %s", Screen::get()->getActiveShaderName(), Screen::get()->getCurrentPresetName());
|
||||
Overlay::showNotification(msg);
|
||||
}
|
||||
}
|
||||
if (next_shader) {
|
||||
consumed = true;
|
||||
}
|
||||
next_shader_prev = next_shader;
|
||||
|
||||
// F8 — Pròxim preset del shader actiu
|
||||
bool next_preset = Ji::keyPressed(KeyConfig::scancode("next_shader_preset"));
|
||||
if (next_preset && !next_preset_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("next_shader_preset", next_preset_prev, [] {
|
||||
if (Screen::get()->nextPreset()) {
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg), Locale::get("notifications.preset_fmt"), Screen::get()->getCurrentPresetName());
|
||||
Overlay::showNotification(msg);
|
||||
}
|
||||
}
|
||||
if (next_preset) {
|
||||
consumed = true;
|
||||
}
|
||||
next_preset_prev = next_preset;
|
||||
|
||||
// F9 — Cicla filtre de textura (NEAREST ↔ LINEAR), sempre aplicat
|
||||
bool texture_filter = Ji::keyPressed(KeyConfig::scancode("cycle_texture_filter"));
|
||||
if (texture_filter && !texture_filter_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("cycle_texture_filter", texture_filter_prev, [] {
|
||||
Screen::get()->cycleTextureFilter(+1);
|
||||
Overlay::showNotification(Options::video.texture_filter == Options::TextureFilter::LINEAR
|
||||
? Locale::get("notifications.filter_linear")
|
||||
: Locale::get("notifications.filter_nearest"));
|
||||
}
|
||||
if (texture_filter) {
|
||||
consumed = true;
|
||||
}
|
||||
texture_filter_prev = texture_filter;
|
||||
|
||||
// F10 — Toggle render info (FPS, driver, shader)
|
||||
bool render_info = Ji::keyPressed(KeyConfig::scancode("toggle_render_info"));
|
||||
if (render_info && !render_info_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("toggle_render_info", render_info_prev, [] {
|
||||
Overlay::toggleRenderInfo();
|
||||
}
|
||||
if (render_info) {
|
||||
consumed = true;
|
||||
}
|
||||
render_info_prev = render_info;
|
||||
|
||||
});
|
||||
return consumed;
|
||||
}
|
||||
|
||||
|
||||
+126
-100
@@ -60,10 +60,10 @@ namespace Menu {
|
||||
const char* label;
|
||||
ItemKind kind;
|
||||
std::function<std::string()> get_value; // opcional
|
||||
std::function<void(int dir)> change; // per TOGGLE/CYCLE/INT_RANGE
|
||||
std::function<void()> enter; // per SUBMENU i ACTION
|
||||
SDL_Scancode* scancode{nullptr}; // per KEY_BIND
|
||||
std::function<bool()> visible; // nullptr ⇒ sempre visible
|
||||
std::function<void(int dir)> change; // per TOGGLE/CYCLE/INT_RANGE
|
||||
std::function<void()> enter; // per SUBMENU i ACTION
|
||||
SDL_Scancode* scancode{nullptr}; // per KEY_BIND
|
||||
std::function<bool()> visible; // nullptr ⇒ sempre visible
|
||||
};
|
||||
|
||||
struct Page {
|
||||
@@ -448,27 +448,30 @@ namespace Menu {
|
||||
capturing = nullptr;
|
||||
}
|
||||
|
||||
void handleKey(SDL_Scancode sc) {
|
||||
if (!isOpen()) {
|
||||
return;
|
||||
static void backOrClose() {
|
||||
if (stack.size() > 1) {
|
||||
popPage();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
Page& page = stack.back();
|
||||
if (page.items.empty()) {
|
||||
// Pàgina buida — només backspace surt
|
||||
if (sc == SDL_SCANCODE_BACKSPACE) {
|
||||
if (stack.size() > 1) {
|
||||
popPage();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
// Activació d'un ítem (RETURN/KP_ENTER): SUBMENU/ACTION criden enter,
|
||||
// KEY_BIND inicia captura, la resta avança change(+1).
|
||||
static void activateItem(Item& item) {
|
||||
if (item.kind == ItemKind::SUBMENU || item.kind == ItemKind::ACTION) {
|
||||
if (item.enter) {
|
||||
item.enter();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Si el cursor està sobre un ítem ocultat (p. ex. una acció anterior el va ocultar),
|
||||
// reubica'l al pròxim visible abans de processar l'entrada.
|
||||
if (!isVisible(page.items[page.cursor])) {
|
||||
page.cursor = nextVisibleCursor(page, page.cursor, +1);
|
||||
} else if (item.kind == ItemKind::KEY_BIND) {
|
||||
capturing = item.scancode;
|
||||
} else if (item.change) {
|
||||
item.change(+1);
|
||||
}
|
||||
}
|
||||
|
||||
static void applyKeyToItem(Page& page, SDL_Scancode sc) {
|
||||
Item& item = page.items[page.cursor];
|
||||
switch (sc) {
|
||||
case SDL_SCANCODE_UP:
|
||||
page.cursor = nextVisibleCursor(page, page.cursor, -1);
|
||||
@@ -477,43 +480,44 @@ namespace Menu {
|
||||
page.cursor = nextVisibleCursor(page, page.cursor, +1);
|
||||
break;
|
||||
case SDL_SCANCODE_LEFT:
|
||||
if (page.items[page.cursor].kind != ItemKind::SUBMENU &&
|
||||
page.items[page.cursor].change) {
|
||||
page.items[page.cursor].change(-1);
|
||||
if (item.kind != ItemKind::SUBMENU && item.change) {
|
||||
item.change(-1);
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_RIGHT:
|
||||
if (page.items[page.cursor].kind != ItemKind::SUBMENU &&
|
||||
page.items[page.cursor].change) {
|
||||
page.items[page.cursor].change(+1);
|
||||
if (item.kind != ItemKind::SUBMENU && item.change) {
|
||||
item.change(+1);
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_RETURN:
|
||||
case SDL_SCANCODE_KP_ENTER:
|
||||
if (page.items[page.cursor].kind == ItemKind::SUBMENU ||
|
||||
page.items[page.cursor].kind == ItemKind::ACTION) {
|
||||
if (page.items[page.cursor].enter) {
|
||||
page.items[page.cursor].enter();
|
||||
}
|
||||
} else if (page.items[page.cursor].kind == ItemKind::KEY_BIND) {
|
||||
capturing = page.items[page.cursor].scancode;
|
||||
} else if (page.items[page.cursor].change) {
|
||||
page.items[page.cursor].change(+1);
|
||||
}
|
||||
activateItem(item);
|
||||
break;
|
||||
case SDL_SCANCODE_BACKSPACE:
|
||||
if (stack.size() > 1) {
|
||||
popPage();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
backOrClose();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Després de qualsevol acció, si el cursor quedara sobre un ítem ocult
|
||||
// (possible si una acció ha canviat la visibilitat pròpia de l'ítem actual,
|
||||
// edge case defensiu), salta al següent visible.
|
||||
}
|
||||
|
||||
void handleKey(SDL_Scancode sc) {
|
||||
if (!isOpen()) {
|
||||
return;
|
||||
}
|
||||
Page& page = stack.back();
|
||||
if (page.items.empty()) {
|
||||
if (sc == SDL_SCANCODE_BACKSPACE) {
|
||||
backOrClose();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!isVisible(page.items[page.cursor])) {
|
||||
page.cursor = nextVisibleCursor(page, page.cursor, +1);
|
||||
}
|
||||
applyKeyToItem(page, sc);
|
||||
// Defensa: si una acció ha amagat l'ítem que tenim sota el cursor,
|
||||
// saltem al pròxim visible.
|
||||
if (!stack.empty()) {
|
||||
Page& top = stack.back();
|
||||
if (!top.items.empty() && !isVisible(top.items[top.cursor])) {
|
||||
@@ -522,6 +526,9 @@ namespace Menu {
|
||||
}
|
||||
}
|
||||
|
||||
// Forward decl: renderOneItem viu sota renderPageContent però aquest l'ha de cridar.
|
||||
static void renderOneItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max);
|
||||
|
||||
// Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip.
|
||||
// box_x/box_y són les coordenades de la caixa (per calcular posicions relatives);
|
||||
// clip_x_min/clip_x_max limiten on es dibuixa text i la línia separadora.
|
||||
@@ -560,69 +567,88 @@ namespace Menu {
|
||||
return;
|
||||
}
|
||||
|
||||
int y_slot = 0; // índex de fila visible (independent de l'índex real de l'ítem)
|
||||
int y_slot = 0;
|
||||
for (size_t i = 0; i < page.items.size(); i++) {
|
||||
const Item& item = page.items[i];
|
||||
if (!isVisible(item)) {
|
||||
continue;
|
||||
}
|
||||
int y = items_y + (y_slot * ITEM_SPACING);
|
||||
const int Y = items_y + (y_slot * ITEM_SPACING);
|
||||
++y_slot;
|
||||
bool selected = (static_cast<int>(i) == page.cursor);
|
||||
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
|
||||
|
||||
// ACTION: sense valor a la dreta — centrem el label amb el cursor just a l'esquerra.
|
||||
if (item.kind == ItemKind::ACTION) {
|
||||
int lw = font->width(item.label);
|
||||
int lx = box_x + ((BOX_W - lw) / 2) + x_offset;
|
||||
if (selected) {
|
||||
font->drawClipped(pixel_data, lx - font->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
font->drawClipped(pixel_data, lx, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
font->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
|
||||
font->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
|
||||
if (item.kind == ItemKind::SUBMENU) {
|
||||
const char* arrow = ">>";
|
||||
int aw = font->width(arrow);
|
||||
Uint32 ac = selected ? CURSOR_COLOR : VALUE_COLOR;
|
||||
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - aw + x_offset, y, arrow, ac, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
} else if (item.kind == ItemKind::KEY_BIND) {
|
||||
bool this_capturing = (capturing == item.scancode);
|
||||
const char* text = nullptr;
|
||||
if (this_capturing) {
|
||||
text = Locale::get("menu.values.press_key");
|
||||
} else if (item.scancode != nullptr) {
|
||||
text = SDL_GetScancodeName(*item.scancode);
|
||||
} else {
|
||||
text = "";
|
||||
}
|
||||
if ((text == nullptr) || (*text == 0)) {
|
||||
text = Locale::get("menu.values.unknown");
|
||||
}
|
||||
int tw = font->width(text);
|
||||
Uint32 tc = 0;
|
||||
if (this_capturing) {
|
||||
tc = 0xFF00FFFF;
|
||||
} else {
|
||||
tc = selected ? CURSOR_COLOR : VALUE_COLOR;
|
||||
}
|
||||
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - tw + x_offset, y, text, tc, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
} else if (item.get_value) {
|
||||
std::string value = item.get_value();
|
||||
int value_w = font->width(value.c_str());
|
||||
Uint32 value_color = selected ? CURSOR_COLOR : VALUE_COLOR;
|
||||
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - value_w + x_offset, y, value.c_str(), value_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
const bool SELECTED = (static_cast<int>(i) == page.cursor);
|
||||
renderOneItem(pixel_data, item, SELECTED, box_x, Y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
}
|
||||
|
||||
static auto keybindText(const Item& item, bool this_capturing) -> const char* {
|
||||
const char* text = nullptr;
|
||||
if (this_capturing) {
|
||||
text = Locale::get("menu.values.press_key");
|
||||
} else if (item.scancode != nullptr) {
|
||||
text = SDL_GetScancodeName(*item.scancode);
|
||||
} else {
|
||||
text = "";
|
||||
}
|
||||
if ((text == nullptr) || (*text == 0)) {
|
||||
text = Locale::get("menu.values.unknown");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
static void renderActionItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
|
||||
const Uint32 LABEL_COLOR_LOCAL = selected ? CURSOR_COLOR : LABEL_COLOR;
|
||||
const int LW = font->width(item.label);
|
||||
const int LX = box_x + ((BOX_W - LW) / 2) + x_offset;
|
||||
if (selected) {
|
||||
font->drawClipped(pixel_data, LX - font->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
font->drawClipped(pixel_data, LX, y, item.label, LABEL_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
|
||||
static auto keybindColor(bool this_capturing, bool selected) -> Uint32 {
|
||||
if (this_capturing) {
|
||||
return 0xFF00FFFF;
|
||||
}
|
||||
return selected ? CURSOR_COLOR : VALUE_COLOR;
|
||||
}
|
||||
|
||||
static void renderItemValue(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
|
||||
if (item.kind == ItemKind::SUBMENU) {
|
||||
const char* arrow = ">>";
|
||||
const int AW = font->width(arrow);
|
||||
const Uint32 AC = selected ? CURSOR_COLOR : VALUE_COLOR;
|
||||
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - AW + x_offset, y, arrow, AC, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
return;
|
||||
}
|
||||
if (item.kind == ItemKind::KEY_BIND) {
|
||||
const bool THIS_CAPTURING = (capturing == item.scancode);
|
||||
const char* text = keybindText(item, THIS_CAPTURING);
|
||||
const int TW = font->width(text);
|
||||
const Uint32 TC = keybindColor(THIS_CAPTURING, selected);
|
||||
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - TW + x_offset, y, text, TC, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
return;
|
||||
}
|
||||
if (item.get_value) {
|
||||
const std::string VALUE = item.get_value();
|
||||
const int VALUE_W = font->width(VALUE.c_str());
|
||||
const Uint32 VALUE_COLOR_LOCAL = selected ? CURSOR_COLOR : VALUE_COLOR;
|
||||
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - VALUE_W + x_offset, y, VALUE.c_str(), VALUE_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
}
|
||||
|
||||
static void renderOneItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
|
||||
if (item.kind == ItemKind::ACTION) {
|
||||
renderActionItem(pixel_data, item, selected, box_x, y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
return;
|
||||
}
|
||||
const Uint32 LABEL_COLOR_LOCAL = selected ? CURSOR_COLOR : LABEL_COLOR;
|
||||
if (selected) {
|
||||
font->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
font->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, LABEL_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
renderItemValue(pixel_data, item, selected, box_x, y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
|
||||
void render(Uint32* pixel_data) {
|
||||
if (!isVisible() || !font || (pixel_data == nullptr)) {
|
||||
return;
|
||||
|
||||
+218
-235
@@ -114,258 +114,241 @@ namespace Overlay {
|
||||
}
|
||||
}
|
||||
|
||||
static void updateNotifFsm(Notification& notif, float dt) {
|
||||
switch (notif.status) {
|
||||
case Status::RISING:
|
||||
notif.anim += SLIDE_SPEED * dt;
|
||||
if (notif.anim >= 1.0F) {
|
||||
notif.anim = 1.0F;
|
||||
notif.status = Status::STAY;
|
||||
notif.timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case Status::STAY:
|
||||
notif.timer += dt;
|
||||
if (notif.timer >= notif.duration) {
|
||||
notif.status = Status::VANISHING;
|
||||
}
|
||||
break;
|
||||
case Status::VANISHING:
|
||||
notif.anim -= SLIDE_SPEED * dt;
|
||||
if (notif.anim <= 0.0F) {
|
||||
notif.status = Status::FINISHED;
|
||||
}
|
||||
break;
|
||||
case Status::FINISHED:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void computeNotifBoxPos(const Notification& notif, int& box_x, int& box_y) {
|
||||
switch (notif.pos) {
|
||||
case NotifPosition::TOP_LEFT_SLIDE:
|
||||
box_x = NOTIF_MARGIN_X - static_cast<int>((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X));
|
||||
box_y = NOTIF_MARGIN_Y;
|
||||
break;
|
||||
case NotifPosition::TOP_CENTER_DROP:
|
||||
box_x = (SCREEN_W - notif.box_w) / 2;
|
||||
box_y = NOTIF_MARGIN_Y - static_cast<int>((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void drawNotifTextLine(Uint32* pixel_data, const std::string& line, int line_x, int line_y, const Notification& notif) {
|
||||
if (notif.style == NotifStyle::SHADOW) {
|
||||
font->draw(pixel_data, line_x + 1, line_y + 1, line.c_str(), notif.accent_color);
|
||||
} else if (notif.style == NotifStyle::OUTLINE) {
|
||||
font->draw(pixel_data, line_x, line_y - 1, line.c_str(), notif.accent_color);
|
||||
font->draw(pixel_data, line_x, line_y + 1, line.c_str(), notif.accent_color);
|
||||
font->draw(pixel_data, line_x - 1, line_y, line.c_str(), notif.accent_color);
|
||||
font->draw(pixel_data, line_x + 1, line_y, line.c_str(), notif.accent_color);
|
||||
}
|
||||
font->draw(pixel_data, line_x, line_y, line.c_str(), notif.text_color);
|
||||
}
|
||||
|
||||
static void renderOneNotification(Uint32* pixel_data, const Notification& notif) {
|
||||
int box_x = 0;
|
||||
int box_y = 0;
|
||||
computeNotifBoxPos(notif, box_x, box_y);
|
||||
if (notif.style == NotifStyle::BOX) {
|
||||
drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color);
|
||||
}
|
||||
const int LINE_H = font->charHeight();
|
||||
int line_y = box_y + NOTIF_PADDING_V;
|
||||
for (const auto& line : notif.lines) {
|
||||
const int LINE_W = font->width(line.c_str());
|
||||
const int LINE_X = box_x + ((notif.box_w - LINE_W) / 2);
|
||||
drawNotifTextLine(pixel_data, line, LINE_X, line_y, notif);
|
||||
line_y += LINE_H + 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void updateRenderInfoFsm(float dt) {
|
||||
const auto DESIRED = Options::render_info.position;
|
||||
if (DESIRED == info_visible_pos) {
|
||||
if (info_anim < 1.0F) {
|
||||
info_anim = std::min(info_anim + (INFO_SLIDE_SPEED * dt), 1.0F);
|
||||
}
|
||||
} else if (info_visible_pos == Options::RenderInfoPosition::OFF) {
|
||||
info_visible_pos = DESIRED;
|
||||
info_anim = 0.0F;
|
||||
} else {
|
||||
info_anim -= INFO_SLIDE_SPEED * dt;
|
||||
if (info_anim <= 0.0F) {
|
||||
info_anim = 0.0F;
|
||||
info_visible_pos = DESIRED;
|
||||
}
|
||||
}
|
||||
for (auto& seg : info_segments) {
|
||||
const float TARGET = seg.visible ? 1.0F : 0.0F;
|
||||
if (seg.anim < TARGET) {
|
||||
seg.anim = std::min(seg.anim + (SEG_SPEED * dt), TARGET);
|
||||
} else if (seg.anim > TARGET) {
|
||||
seg.anim = std::max(seg.anim - (SEG_SPEED * dt), TARGET);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static auto computeInfoTotalWidth(int digit_cell) -> float {
|
||||
float total_w = 0.0F;
|
||||
for (const auto& seg : info_segments) {
|
||||
if (seg.anim > 0.0F && !seg.text.empty()) {
|
||||
const int W = seg.mono_digits
|
||||
? font->widthMonoDigits(seg.text.c_str(), digit_cell)
|
||||
: font->width(seg.text.c_str());
|
||||
total_w += static_cast<float>(W) * Easing::outQuad(seg.anim);
|
||||
}
|
||||
}
|
||||
return total_w;
|
||||
}
|
||||
|
||||
static void drawInfoSegment(Uint32* pixel_data, const InfoSegment& seg, int xi, int info_y, int digit_cell) {
|
||||
if (seg.mono_digits) {
|
||||
font->drawMonoDigits(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color, digit_cell);
|
||||
font->drawMonoDigits(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color, digit_cell);
|
||||
} else {
|
||||
font->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color);
|
||||
font->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color);
|
||||
}
|
||||
}
|
||||
|
||||
static void renderRenderInfo(Uint32* pixel_data) {
|
||||
if (info_visible_pos == Options::RenderInfoPosition::OFF || info_anim <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
const int DIGIT_CELL = font->charBoxWidth() - 1;
|
||||
const float TOTAL_W = computeInfoTotalWidth(DIGIT_CELL);
|
||||
if (TOTAL_W <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
const float EASED_Y = Easing::outQuad(info_anim);
|
||||
const int CH = font->charHeight();
|
||||
const int FINAL_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? 1 : SCREEN_H - CH - 1;
|
||||
const int START_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? -CH - 1 : SCREEN_H;
|
||||
const int INFO_Y = START_Y + static_cast<int>(static_cast<float>(FINAL_Y - START_Y) * EASED_Y);
|
||||
float cur_x = (SCREEN_W - TOTAL_W) / 2.0F;
|
||||
for (const auto& seg : info_segments) {
|
||||
if (seg.anim <= 0.01F || seg.text.empty()) {
|
||||
continue;
|
||||
}
|
||||
const int XI = static_cast<int>(cur_x);
|
||||
const int SEG_W = seg.mono_digits
|
||||
? font->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
|
||||
: font->width(seg.text.c_str());
|
||||
drawInfoSegment(pixel_data, seg, XI, INFO_Y, DIGIT_CELL);
|
||||
cur_x += static_cast<float>(SEG_W) * Easing::outQuad(seg.anim);
|
||||
}
|
||||
}
|
||||
|
||||
static void renderPauseIndicator(Uint32* pixel_data) {
|
||||
if ((Director::get() == nullptr) || !Director::get()->isPaused()) {
|
||||
return;
|
||||
}
|
||||
const char* pause_text = Locale::get("notifications.pause");
|
||||
const int W = font->width(pause_text);
|
||||
const int X = SCREEN_W - W - 4;
|
||||
const int Y = 4;
|
||||
font->draw(pixel_data, X, Y - 1, pause_text, 0xFFFFFFFF);
|
||||
font->draw(pixel_data, X, Y + 1, pause_text, 0xFFFFFFFF);
|
||||
font->draw(pixel_data, X - 1, Y, pause_text, 0xFFFFFFFF);
|
||||
font->draw(pixel_data, X + 1, Y, pause_text, 0xFFFFFFFF);
|
||||
font->draw(pixel_data, X, Y, pause_text, 0xFF0000FF);
|
||||
}
|
||||
|
||||
static void emitCreditsLines(const char* role_key, const char* name_key) {
|
||||
showNotification(
|
||||
{std::string(Locale::get(role_key)), std::string(Locale::get(name_key))},
|
||||
CREDITS_HOLD,
|
||||
NotifPosition::TOP_CENTER_DROP,
|
||||
NotifStyle::OUTLINE,
|
||||
CREDITS_BG,
|
||||
CREDITS_FG);
|
||||
}
|
||||
|
||||
static void advanceCredits(float dt) {
|
||||
if (credits_phase == CreditsPhase::IDLE) {
|
||||
return;
|
||||
}
|
||||
credits_timer += dt;
|
||||
switch (credits_phase) {
|
||||
case CreditsPhase::DELAY:
|
||||
if (credits_timer >= CREDITS_DELAY) {
|
||||
emitCreditsLines("credits.port_role", "credits.port_name");
|
||||
credits_phase = CreditsPhase::PLAYING_1;
|
||||
credits_timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::PLAYING_1:
|
||||
if (notifications.empty()) {
|
||||
credits_phase = CreditsPhase::GAP;
|
||||
credits_timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::GAP:
|
||||
if (credits_timer >= CREDITS_GAP) {
|
||||
emitCreditsLines("credits.modern_role", "credits.modern_name");
|
||||
credits_phase = CreditsPhase::PLAYING_2;
|
||||
credits_timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::PLAYING_2:
|
||||
if (notifications.empty()) {
|
||||
credits_phase = CreditsPhase::IDLE;
|
||||
credits_timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::IDLE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void render(Uint32* pixel_data) {
|
||||
if (!font || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
const Uint32 NOW = SDL_GetTicks();
|
||||
const float DT = static_cast<float>(NOW - last_ticks) / 1000.0F;
|
||||
last_ticks = NOW;
|
||||
|
||||
// Calcula delta time
|
||||
Uint32 now = SDL_GetTicks();
|
||||
float dt = static_cast<float>(now - last_ticks) / 1000.0F;
|
||||
last_ticks = now;
|
||||
|
||||
// Actualitza i pinta cada notificació
|
||||
for (auto& notif : notifications) {
|
||||
switch (notif.status) {
|
||||
case Status::RISING:
|
||||
notif.anim += SLIDE_SPEED * dt;
|
||||
if (notif.anim >= 1.0F) {
|
||||
notif.anim = 1.0F;
|
||||
notif.status = Status::STAY;
|
||||
notif.timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
|
||||
case Status::STAY:
|
||||
notif.timer += dt;
|
||||
if (notif.timer >= notif.duration) {
|
||||
notif.status = Status::VANISHING;
|
||||
}
|
||||
break;
|
||||
|
||||
case Status::VANISHING:
|
||||
notif.anim -= SLIDE_SPEED * dt;
|
||||
if (notif.anim <= 0.0F) {
|
||||
notif.status = Status::FINISHED;
|
||||
}
|
||||
break;
|
||||
|
||||
case Status::FINISHED:
|
||||
break;
|
||||
}
|
||||
|
||||
if (notif.status == Status::FINISHED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Posició segons el tipus
|
||||
int box_x = 0;
|
||||
int box_y = 0;
|
||||
switch (notif.pos) {
|
||||
case NotifPosition::TOP_LEFT_SLIDE: {
|
||||
int target_x = NOTIF_MARGIN_X;
|
||||
int target_y = NOTIF_MARGIN_Y;
|
||||
box_x = target_x - static_cast<int>((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X));
|
||||
box_y = target_y;
|
||||
break;
|
||||
}
|
||||
case NotifPosition::TOP_CENTER_DROP: {
|
||||
int target_y = NOTIF_MARGIN_Y;
|
||||
box_x = (SCREEN_W - notif.box_w) / 2;
|
||||
// Baixa des de sobre de la pantalla fins a target_y
|
||||
box_y = target_y - static_cast<int>((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Pinta fons (si BOX)
|
||||
if (notif.style == NotifStyle::BOX) {
|
||||
drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color);
|
||||
}
|
||||
|
||||
// Pinta el text línia a línia (amb ombra o contorn segons style)
|
||||
int line_h = font->charHeight();
|
||||
int line_y = box_y + NOTIF_PADDING_V;
|
||||
for (const auto& line : notif.lines) {
|
||||
int line_w = font->width(line.c_str());
|
||||
int line_x = box_x + ((notif.box_w - line_w) / 2); // centrat dins la caixa
|
||||
if (notif.style == NotifStyle::SHADOW) {
|
||||
font->draw(pixel_data, line_x + 1, line_y + 1, line.c_str(), notif.accent_color);
|
||||
} else if (notif.style == NotifStyle::OUTLINE) {
|
||||
// Contorn 4-direccional (N, S, E, W)
|
||||
font->draw(pixel_data, line_x, line_y - 1, line.c_str(), notif.accent_color);
|
||||
font->draw(pixel_data, line_x, line_y + 1, line.c_str(), notif.accent_color);
|
||||
font->draw(pixel_data, line_x - 1, line_y, line.c_str(), notif.accent_color);
|
||||
font->draw(pixel_data, line_x + 1, line_y, line.c_str(), notif.accent_color);
|
||||
}
|
||||
font->draw(pixel_data, line_x, line_y, line.c_str(), notif.text_color);
|
||||
line_y += line_h + 1;
|
||||
updateNotifFsm(notif, DT);
|
||||
if (notif.status != Status::FINISHED) {
|
||||
renderOneNotification(pixel_data, notif);
|
||||
}
|
||||
}
|
||||
|
||||
// Render info (FPS, driver, shader) — animat amb slide vertical
|
||||
// State machine: visible_pos s'actualitza cap a DESIRED quan anim arriba a 0
|
||||
{
|
||||
const auto DESIRED = Options::render_info.position;
|
||||
if (DESIRED == info_visible_pos) {
|
||||
// Mateix lloc: entra fins a 1
|
||||
if (info_anim < 1.0F) {
|
||||
info_anim += INFO_SLIDE_SPEED * dt;
|
||||
info_anim = std::min(info_anim, 1.0F);
|
||||
}
|
||||
} else {
|
||||
// Canvi: si visible_pos està OFF, commuta directament
|
||||
if (info_visible_pos == Options::RenderInfoPosition::OFF) {
|
||||
info_visible_pos = DESIRED;
|
||||
info_anim = 0.0F;
|
||||
} else {
|
||||
// Ix del lloc actual
|
||||
info_anim -= INFO_SLIDE_SPEED * dt;
|
||||
if (info_anim <= 0.0F) {
|
||||
info_anim = 0.0F;
|
||||
info_visible_pos = DESIRED;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateRenderInfoFsm(DT);
|
||||
renderRenderInfo(pixel_data);
|
||||
|
||||
// Actualitza animacions individuals dels segments
|
||||
for (auto& seg : info_segments) {
|
||||
float target = seg.visible ? 1.0F : 0.0F;
|
||||
if (seg.anim < target) {
|
||||
seg.anim += SEG_SPEED * dt;
|
||||
seg.anim = std::min(seg.anim, target);
|
||||
} else if (seg.anim > target) {
|
||||
seg.anim -= SEG_SPEED * dt;
|
||||
seg.anim = std::max(seg.anim, target);
|
||||
}
|
||||
}
|
||||
|
||||
// Render si hi ha alguna cosa visible
|
||||
if (info_visible_pos != Options::RenderInfoPosition::OFF && info_anim > 0.0F) {
|
||||
const int DIGIT_CELL = font->charBoxWidth() - 1; // amplada uniforme per dígit
|
||||
|
||||
// Calcula amplada total interpolant cada segment per la seva anim
|
||||
float total_w = 0.0F;
|
||||
for (const auto& seg : info_segments) {
|
||||
if (seg.anim > 0.0F && !seg.text.empty()) {
|
||||
int w = seg.mono_digits
|
||||
? font->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
|
||||
: font->width(seg.text.c_str());
|
||||
total_w += w * Easing::outQuad(seg.anim);
|
||||
}
|
||||
}
|
||||
if (total_w > 0.0F) {
|
||||
float eased_y = Easing::outQuad(info_anim);
|
||||
int ch = font->charHeight();
|
||||
int final_y;
|
||||
int start_y;
|
||||
if (info_visible_pos == Options::RenderInfoPosition::TOP) {
|
||||
final_y = 1;
|
||||
start_y = -ch - 1;
|
||||
} else {
|
||||
final_y = SCREEN_H - ch - 1;
|
||||
start_y = SCREEN_H;
|
||||
}
|
||||
int info_y = start_y + static_cast<int>((final_y - start_y) * eased_y);
|
||||
|
||||
// Dibuixa cada segment en la seva posició x acumulada
|
||||
float cur_x = (SCREEN_W - total_w) / 2.0F;
|
||||
for (const auto& seg : info_segments) {
|
||||
if (seg.anim > 0.01F && !seg.text.empty()) {
|
||||
int xi = static_cast<int>(cur_x);
|
||||
int seg_w = seg.mono_digits
|
||||
? font->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
|
||||
: font->width(seg.text.c_str());
|
||||
if (seg.mono_digits) {
|
||||
font->drawMonoDigits(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color, DIGIT_CELL);
|
||||
font->drawMonoDigits(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color, DIGIT_CELL);
|
||||
} else {
|
||||
font->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color);
|
||||
font->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color);
|
||||
}
|
||||
cur_x += seg_w * Easing::outQuad(seg.anim);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Elimina les acabades
|
||||
std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; });
|
||||
|
||||
// Si la notificació d'ESC ha desaparegut, reseteja l'estat
|
||||
if (esc_waiting && notifications.empty()) {
|
||||
esc_waiting = false;
|
||||
}
|
||||
|
||||
// Indicador de pausa persistent (cantó superior dret)
|
||||
if ((Director::get() != nullptr) && Director::get()->isPaused()) {
|
||||
const char* pause_text = Locale::get("notifications.pause");
|
||||
int w = font->width(pause_text);
|
||||
int x = SCREEN_W - w - 4;
|
||||
int y = 4;
|
||||
// Contorn blanc 4-direccional
|
||||
font->draw(pixel_data, x, y - 1, pause_text, 0xFFFFFFFF);
|
||||
font->draw(pixel_data, x, y + 1, pause_text, 0xFFFFFFFF);
|
||||
font->draw(pixel_data, x - 1, y, pause_text, 0xFFFFFFFF);
|
||||
font->draw(pixel_data, x + 1, y, pause_text, 0xFFFFFFFF);
|
||||
// Text en roig
|
||||
font->draw(pixel_data, x, y, pause_text, 0xFF0000FF);
|
||||
}
|
||||
renderPauseIndicator(pixel_data);
|
||||
advanceCredits(DT);
|
||||
|
||||
// Crèdits seqüencials — dispara notificacions TOP_CENTER_DROP una darrere l'altra.
|
||||
if (credits_phase != CreditsPhase::IDLE) {
|
||||
credits_timer += dt;
|
||||
switch (credits_phase) {
|
||||
case CreditsPhase::DELAY:
|
||||
if (credits_timer >= CREDITS_DELAY) {
|
||||
showNotification(
|
||||
{std::string(Locale::get("credits.port_role")),
|
||||
std::string(Locale::get("credits.port_name"))},
|
||||
CREDITS_HOLD,
|
||||
NotifPosition::TOP_CENTER_DROP,
|
||||
NotifStyle::OUTLINE,
|
||||
CREDITS_BG,
|
||||
CREDITS_FG);
|
||||
credits_phase = CreditsPhase::PLAYING_1;
|
||||
credits_timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::PLAYING_1:
|
||||
if (notifications.empty()) {
|
||||
credits_phase = CreditsPhase::GAP;
|
||||
credits_timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::GAP:
|
||||
if (credits_timer >= CREDITS_GAP) {
|
||||
showNotification(
|
||||
{std::string(Locale::get("credits.modern_role")),
|
||||
std::string(Locale::get("credits.modern_name"))},
|
||||
CREDITS_HOLD,
|
||||
NotifPosition::TOP_CENTER_DROP,
|
||||
NotifStyle::OUTLINE,
|
||||
CREDITS_BG,
|
||||
CREDITS_FG);
|
||||
credits_phase = CreditsPhase::PLAYING_2;
|
||||
credits_timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::PLAYING_2:
|
||||
if (notifications.empty()) {
|
||||
credits_phase = CreditsPhase::IDLE;
|
||||
credits_timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::IDLE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Neteja notificacions finalitzades
|
||||
std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; });
|
||||
|
||||
// Menú flotant per damunt de tot (isVisible inclou l'animació de tancament)
|
||||
if (Menu::isVisible()) {
|
||||
Menu::render(pixel_data);
|
||||
}
|
||||
|
||||
+65
-177
@@ -1,5 +1,6 @@
|
||||
#include "core/rendering/text.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -158,60 +159,57 @@ void Text::loadBitmap(const char* gif_file) {
|
||||
|
||||
// --- Renderitzat ---
|
||||
|
||||
auto Text::resolveGlyph(uint32_t cp) const -> const GlyphInfo* {
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it != glyphs_.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
it = glyphs_.find('?');
|
||||
return (it != glyphs_.end()) ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
void Text::blitGlyph(Uint32* pixel_data, int dst_x, int dst_y, const GlyphInfo& glyph, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const {
|
||||
const int GY_START = std::max(0, clip_y_min - dst_y);
|
||||
const int GY_END = std::min(box_height_, clip_y_max - dst_y);
|
||||
const int GX_START = std::max(0, clip_x_min - dst_x);
|
||||
const int GX_END = std::min(glyph.w, clip_x_max - dst_x);
|
||||
for (int gy = GY_START; gy < GY_END; gy++) {
|
||||
const int SRC_Y = glyph.y + gy;
|
||||
if (SRC_Y >= bitmap_height_) {
|
||||
continue;
|
||||
}
|
||||
const int DST_ROW = dst_y + gy;
|
||||
for (int gx = GX_START; gx < GX_END; gx++) {
|
||||
const int SRC_X = glyph.x + gx;
|
||||
if (SRC_X >= bitmap_width_) {
|
||||
continue;
|
||||
}
|
||||
const Uint8 PIXEL = bitmap_[SRC_X + (SRC_Y * bitmap_width_)];
|
||||
if (PIXEL != 0) {
|
||||
pixel_data[(dst_x + gx) + (DST_ROW * SCREEN_WIDTH)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const {
|
||||
if (bitmap_.empty() || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
if (it == glyphs_.end()) {
|
||||
cursor_x += box_width_;
|
||||
continue;
|
||||
}
|
||||
const GlyphInfo* glyph = resolveGlyph(cp);
|
||||
if (glyph == nullptr) {
|
||||
cursor_x += box_width_;
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& glyph = it->second;
|
||||
|
||||
// Pinta glifo pixel a pixel
|
||||
for (int gy = 0; gy < box_height_; gy++) {
|
||||
int dst_y = y + gy;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int gx = 0; gx < glyph.w; gx++) {
|
||||
int dst_x = cursor_x + gx;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int src_x = glyph.x + gx;
|
||||
int src_y = glyph.y + gy;
|
||||
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
|
||||
// Píxel no transparent (índex 0 és fons típicament)
|
||||
if (pixel != 0) {
|
||||
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursor_x += glyph.w + 1; // +1 kerning
|
||||
blitGlyph(pixel_data, cursor_x, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
|
||||
cursor_x += glyph->w + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,70 +223,29 @@ void Text::drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint3
|
||||
if (bitmap_.empty() || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Descart ràpid si el glifo sencer cau fora verticalment
|
||||
if (y + box_height_ <= clip_y_min || y >= clip_y_max) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int X_MIN = std::max(0, clip_x_min);
|
||||
const int X_MAX = std::min(SCREEN_WIDTH, clip_x_max);
|
||||
const int Y_MIN = std::max(0, clip_y_min);
|
||||
const int Y_MAX = std::min(SCREEN_HEIGHT, clip_y_max);
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
if (it == glyphs_.end()) {
|
||||
cursor_x += box_width_;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const auto& glyph = it->second;
|
||||
|
||||
// Si el glifo està completament fora del clip horitzontal, salta
|
||||
if (cursor_x + glyph.w <= clip_x_min || cursor_x >= clip_x_max) {
|
||||
cursor_x += glyph.w + 1;
|
||||
const GlyphInfo* glyph = resolveGlyph(cp);
|
||||
if (glyph == nullptr) {
|
||||
cursor_x += box_width_;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int gy = 0; gy < box_height_; gy++) {
|
||||
int dst_y = y + gy;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
|
||||
continue;
|
||||
}
|
||||
if (dst_y < clip_y_min || dst_y >= clip_y_max) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int gx = 0; gx < glyph.w; gx++) {
|
||||
int dst_x = cursor_x + gx;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
if (dst_x < clip_x_min || dst_x >= clip_x_max) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int src_x = glyph.x + gx;
|
||||
int src_y = glyph.y + gy;
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
|
||||
if (pixel != 0) {
|
||||
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
|
||||
}
|
||||
}
|
||||
if (cursor_x + glyph->w > X_MIN && cursor_x < X_MAX) {
|
||||
blitGlyph(pixel_data, cursor_x, y, *glyph, color, X_MIN, X_MAX, Y_MIN, Y_MAX);
|
||||
}
|
||||
|
||||
cursor_x += glyph.w + 1;
|
||||
cursor_x += glyph->w + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,54 +253,20 @@ void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 c
|
||||
if (bitmap_.empty() || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
if (it == glyphs_.end()) {
|
||||
cursor_x += cell_w;
|
||||
continue;
|
||||
}
|
||||
const GlyphInfo* glyph = resolveGlyph(cp);
|
||||
if (glyph == nullptr) {
|
||||
cursor_x += cell_w;
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& glyph = it->second;
|
||||
// Centra el glif dins la cel·la
|
||||
int glyph_x = cursor_x + ((cell_w - glyph.w) / 2);
|
||||
|
||||
for (int gy = 0; gy < box_height_; gy++) {
|
||||
int dst_y = y + gy;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int gx = 0; gx < glyph.w; gx++) {
|
||||
int dst_x = glyph_x + gx;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int src_x = glyph.x + gx;
|
||||
int src_y = glyph.y + gy;
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
|
||||
if (pixel != 0) {
|
||||
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int GLYPH_X = cursor_x + ((cell_w - glyph->w) / 2);
|
||||
blitGlyph(pixel_data, GLYPH_X, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
|
||||
cursor_x += cell_w;
|
||||
}
|
||||
}
|
||||
@@ -352,62 +275,27 @@ void Text::drawMonoDigits(Uint32* pixel_data, int x, int y, const char* text, Ui
|
||||
if (bitmap_.empty() || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
bool first = true;
|
||||
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
if (it == glyphs_.end()) {
|
||||
if (!first) {
|
||||
cursor_x += 1;
|
||||
}
|
||||
cursor_x += box_width_;
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const auto& glyph = it->second;
|
||||
bool is_digit = (cp >= '0' && cp <= '9');
|
||||
|
||||
if (!first) {
|
||||
cursor_x += 1; // kerning
|
||||
}
|
||||
|
||||
int glyph_x = is_digit ? cursor_x + ((digit_cell_w - glyph.w) / 2) : cursor_x;
|
||||
|
||||
for (int gy = 0; gy < box_height_; gy++) {
|
||||
int dst_y = y + gy;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
|
||||
continue;
|
||||
}
|
||||
for (int gx = 0; gx < glyph.w; gx++) {
|
||||
int dst_x = glyph_x + gx;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
int src_x = glyph.x + gx;
|
||||
int src_y = glyph.y + gy;
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
|
||||
continue;
|
||||
}
|
||||
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
|
||||
if (pixel != 0) {
|
||||
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
|
||||
}
|
||||
}
|
||||
const GlyphInfo* glyph = resolveGlyph(cp);
|
||||
if (glyph == nullptr) {
|
||||
cursor_x += box_width_;
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
cursor_x += is_digit ? digit_cell_w : glyph.w;
|
||||
const bool IS_DIGIT = (cp >= '0' && cp <= '9');
|
||||
const int GLYPH_X = IS_DIGIT ? cursor_x + ((digit_cell_w - glyph->w) / 2) : cursor_x;
|
||||
blitGlyph(pixel_data, GLYPH_X, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
|
||||
cursor_x += IS_DIGIT ? digit_cell_w : glyph->w;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,13 @@ class Text {
|
||||
void loadFont(const char* fnt_file);
|
||||
void loadBitmap(const char* gif_file);
|
||||
|
||||
// Resolt un codepoint al GlyphInfo corresponent o al fallback '?'.
|
||||
// Retorna nullptr si ni el codepoint ni el fallback existeixen.
|
||||
[[nodiscard]] auto resolveGlyph(uint32_t cp) const -> const GlyphInfo*;
|
||||
// Pinta un glif a (dst_x, dst_y) amb clipping per finestra.
|
||||
// Si la finestra és tota la pantalla, passar clip_x_min=0, clip_x_max=SCREEN_WIDTH, idem y.
|
||||
void blitGlyph(Uint32* pixel_data, int dst_x, int dst_y, const GlyphInfo& glyph, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const;
|
||||
|
||||
static constexpr int SCREEN_WIDTH = 320;
|
||||
static constexpr int SCREEN_HEIGHT = 200;
|
||||
};
|
||||
|
||||
@@ -105,6 +105,36 @@ namespace Resource {
|
||||
std::cout << "Resource::Cache: precarregant " << total_count_ << " assets\n";
|
||||
}
|
||||
|
||||
void Cache::stepEachInList(List::Type type, const std::function<void()>& clear_fn, LoadStage next, const std::function<void(size_t)>& load_fn) {
|
||||
auto items = List::get()->getListByType(type);
|
||||
if (stage_index_ == 0) {
|
||||
clear_fn();
|
||||
}
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = next;
|
||||
stage_index_ = 0;
|
||||
return;
|
||||
}
|
||||
load_fn(stage_index_++);
|
||||
}
|
||||
|
||||
void Cache::stepTextFiles() {
|
||||
auto data_items = List::get()->getListByType(List::Type::DATA);
|
||||
auto font_items = List::get()->getListByType(List::Type::FONT);
|
||||
auto items = data_items;
|
||||
items.insert(items.end(), font_items.begin(), font_items.end());
|
||||
if (stage_index_ == 0) {
|
||||
text_files_.clear();
|
||||
}
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::DONE;
|
||||
stage_index_ = 0;
|
||||
std::cout << "Resource::Cache: precarrega completada (" << loaded_count_ << "/" << total_count_ << ")\n";
|
||||
return;
|
||||
}
|
||||
loadOneTextFile(stage_index_++);
|
||||
}
|
||||
|
||||
auto Cache::loadStep(int budget_ms) -> bool {
|
||||
if (stage_ == LoadStage::DONE) {
|
||||
return true;
|
||||
@@ -112,66 +142,21 @@ namespace Resource {
|
||||
|
||||
const Uint64 START_NS = SDL_GetTicksNS();
|
||||
const Uint64 BUDGET_NS = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
|
||||
const auto* list = List::get();
|
||||
|
||||
while (stage_ != LoadStage::DONE) {
|
||||
switch (stage_) {
|
||||
case LoadStage::MUSICS: {
|
||||
auto items = list->getListByType(List::Type::MUSIC);
|
||||
if (stage_index_ == 0) {
|
||||
musics_.clear();
|
||||
}
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::SOUNDS;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneMusic(stage_index_++);
|
||||
case LoadStage::MUSICS:
|
||||
stepEachInList(List::Type::MUSIC, [this] { musics_.clear(); }, LoadStage::SOUNDS, [this](size_t i) { loadOneMusic(i); });
|
||||
break;
|
||||
}
|
||||
case LoadStage::SOUNDS: {
|
||||
auto items = list->getListByType(List::Type::SOUND);
|
||||
if (stage_index_ == 0) {
|
||||
sounds_.clear();
|
||||
}
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::BITMAPS;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneSound(stage_index_++);
|
||||
case LoadStage::SOUNDS:
|
||||
stepEachInList(List::Type::SOUND, [this] { sounds_.clear(); }, LoadStage::BITMAPS, [this](size_t i) { loadOneSound(i); });
|
||||
break;
|
||||
}
|
||||
case LoadStage::BITMAPS: {
|
||||
auto items = list->getListByType(List::Type::BITMAP);
|
||||
if (stage_index_ == 0) {
|
||||
surfaces_.clear();
|
||||
}
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::TEXT_FILES;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneBitmap(stage_index_++);
|
||||
case LoadStage::BITMAPS:
|
||||
stepEachInList(List::Type::BITMAP, [this] { surfaces_.clear(); }, LoadStage::TEXT_FILES, [this](size_t i) { loadOneBitmap(i); });
|
||||
break;
|
||||
}
|
||||
case LoadStage::TEXT_FILES: {
|
||||
auto data_items = list->getListByType(List::Type::DATA);
|
||||
auto font_items = list->getListByType(List::Type::FONT);
|
||||
auto items = data_items;
|
||||
items.insert(items.end(), font_items.begin(), font_items.end());
|
||||
if (stage_index_ == 0) {
|
||||
text_files_.clear();
|
||||
}
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::DONE;
|
||||
stage_index_ = 0;
|
||||
std::cout << "Resource::Cache: precarrega completada (" << loaded_count_ << "/" << total_count_ << ")\n";
|
||||
break;
|
||||
}
|
||||
loadOneTextFile(stage_index_++);
|
||||
case LoadStage::TEXT_FILES:
|
||||
stepTextFiles();
|
||||
break;
|
||||
}
|
||||
case LoadStage::DONE:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/resources/resource_list.hpp"
|
||||
#include "core/resources/resource_types.hpp"
|
||||
|
||||
namespace Resource {
|
||||
@@ -56,6 +58,8 @@ namespace Resource {
|
||||
void loadOneSound(size_t index);
|
||||
void loadOneBitmap(size_t index);
|
||||
void loadOneTextFile(size_t index);
|
||||
void stepEachInList(List::Type type, const std::function<void()>& clear_fn, LoadStage next, const std::function<void(size_t)>& load_fn);
|
||||
void stepTextFiles();
|
||||
|
||||
std::vector<MusicResource> musics_;
|
||||
std::vector<SoundResource> sounds_;
|
||||
|
||||
+107
-137
@@ -136,120 +136,100 @@ void Director::setup() {
|
||||
has_frame_ = false;
|
||||
}
|
||||
|
||||
void Director::applyRestart() {
|
||||
restart_requested_ = false;
|
||||
Audio::get()->stopMusic();
|
||||
Audio::get()->stopAllSounds();
|
||||
initGameContext();
|
||||
Info::ctx.num_piramide = 255;
|
||||
current_scene_.reset();
|
||||
game_state_ = 1;
|
||||
has_frame_ = false;
|
||||
Menu::close();
|
||||
Ji::setInputBlocked(false);
|
||||
}
|
||||
|
||||
void Director::maybeStartTitleCredits() {
|
||||
static bool credits_triggered_ = false;
|
||||
if (credits_triggered_ || Info::ctx.num_piramide != 0) {
|
||||
return;
|
||||
}
|
||||
if (Options::game.show_title_credits) {
|
||||
Overlay::startCredits();
|
||||
}
|
||||
credits_triggered_ = true;
|
||||
}
|
||||
|
||||
auto Director::tickActiveScene() -> bool {
|
||||
if (current_scene_ && (current_scene_->done() || Jg::quitting())) {
|
||||
game_state_ = current_scene_->nextState();
|
||||
current_scene_.reset();
|
||||
}
|
||||
if (!current_scene_) {
|
||||
if (game_state_ == -1 || Jg::quitting()) {
|
||||
return false;
|
||||
}
|
||||
current_scene_ = createNextScene();
|
||||
if (!current_scene_) {
|
||||
return false;
|
||||
}
|
||||
current_scene_->onEnter();
|
||||
last_tick_ms_ = SDL_GetTicks();
|
||||
}
|
||||
|
||||
Ji::update();
|
||||
const Uint32 NOW = SDL_GetTicks();
|
||||
const int DELTA_MS = static_cast<int>(NOW - last_tick_ms_);
|
||||
last_tick_ms_ = NOW;
|
||||
current_scene_->tick(DELTA_MS);
|
||||
|
||||
Jd8::flip();
|
||||
std::memcpy(game_frame_, Jd8::getFramebuffer(), sizeof(game_frame_));
|
||||
has_frame_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Director::iterate() -> bool {
|
||||
if (quit_requested_) {
|
||||
Jg::quitSignal();
|
||||
current_scene_.reset(); // destrueix l'escena actual ordenadament
|
||||
current_scene_.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reinici "suau": processat al començament del frame per no manipular
|
||||
// l'escena des d'una lambda del menú mentre encara s'està executant.
|
||||
if (restart_requested_) {
|
||||
restart_requested_ = false;
|
||||
Audio::get()->stopMusic();
|
||||
Audio::get()->stopAllSounds();
|
||||
// Reinicialitza Info::ctx des d'Options (vides, diners, diamants...)
|
||||
// en lloc de ctx.reset() pla que deixaria vida=0 → jugador mort.
|
||||
initGameContext();
|
||||
// Força l'intro independentment de `piramide_inicial` (que pot estar
|
||||
// configurat a una piràmide intermèdia per a proves ràpides).
|
||||
Info::ctx.num_piramide = 255;
|
||||
current_scene_.reset();
|
||||
game_state_ = 1; // 1 = dispatch via SceneRegistry per num_piramide
|
||||
has_frame_ = false;
|
||||
Menu::close();
|
||||
Ji::setInputBlocked(false); // el menú ho havia bloquejat — cal desfer-ho
|
||||
applyRestart();
|
||||
}
|
||||
|
||||
if (!context_initialized_) {
|
||||
initGameContext();
|
||||
context_initialized_ = true;
|
||||
}
|
||||
|
||||
constexpr Uint32 FRAME_MS_VSYNC = 16; // ~60 FPS amb VSync
|
||||
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
|
||||
|
||||
constexpr Uint32 FRAME_MS_VSYNC = 16;
|
||||
constexpr Uint32 FRAME_MS_NO_VSYNC = 4;
|
||||
const Uint32 FRAME_START = SDL_GetTicks();
|
||||
|
||||
Gamepad::update();
|
||||
KeyRemap::update();
|
||||
GlobalInputs::handle();
|
||||
Mouse::updateCursorVisibility();
|
||||
|
||||
// Bombeig de l'àudio: reomple l'stream de música i para els canals
|
||||
// drenats. Substituïx el callback de SDL_AddTimer de la versió
|
||||
// antiga — imprescindible per al port a emscripten.
|
||||
Audio::update();
|
||||
|
||||
// Dispara els crèdits cinematogràfics la primera vegada que el joc
|
||||
// arriba al menú del títol (Info::ctx.num_piramide == 0).
|
||||
static bool credits_triggered_ = false;
|
||||
if (!credits_triggered_ && Info::ctx.num_piramide == 0) {
|
||||
if (Options::game.show_title_credits) {
|
||||
Overlay::startCredits();
|
||||
}
|
||||
credits_triggered_ = true;
|
||||
}
|
||||
maybeStartTitleCredits();
|
||||
|
||||
// Si l'overlay ja no bloqueja ESC (timeout), desbloquegem
|
||||
if (esc_blocked_ && !Overlay::isEscConsumed()) {
|
||||
esc_blocked_ = false;
|
||||
}
|
||||
|
||||
// Avança l'escena (si no estem pausats). En pausa, es manté l'escena
|
||||
// congelada i re-presentem l'últim frame amb l'overlay fresc per
|
||||
// damunt.
|
||||
if (!paused_) {
|
||||
// Transicions: si l'escena actual ha acabat (o s'ha senyalat
|
||||
// quit), llegim el seu next state i la destruïm per crear la
|
||||
// següent a continuació.
|
||||
if (current_scene_ && (current_scene_->done() || Jg::quitting())) {
|
||||
game_state_ = current_scene_->nextState();
|
||||
current_scene_.reset();
|
||||
if (!tickActiveScene()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Si no hi ha escena activa, construeix la pròxima segons
|
||||
// game_state_ i Info::ctx. Si és impossible (game_state_ == -1,
|
||||
// quit, o state no registrat), eixim del loop.
|
||||
if (!current_scene_) {
|
||||
if (game_state_ == -1 || Jg::quitting()) {
|
||||
return false;
|
||||
}
|
||||
current_scene_ = createNextScene();
|
||||
if (!current_scene_) {
|
||||
return false;
|
||||
}
|
||||
current_scene_->onEnter();
|
||||
last_tick_ms_ = SDL_GetTicks();
|
||||
}
|
||||
|
||||
// Tick de l'escena. Ji::update refresca key_pressed/any_key; el
|
||||
// DELTA_MS és el temps real transcorregut des de l'últim tick.
|
||||
Ji::update();
|
||||
const Uint32 NOW = SDL_GetTicks();
|
||||
const int DELTA_MS = static_cast<int>(NOW - last_tick_ms_);
|
||||
last_tick_ms_ = NOW;
|
||||
current_scene_->tick(DELTA_MS);
|
||||
|
||||
// Converteix `screen` indexat → `pixel_data` ARGB amb la paleta
|
||||
// actual. Jd8::flip ja no fa yield (Phase B.2 eliminà els fibers);
|
||||
// ara només omple el framebuffer perquè el Director l'aprofite.
|
||||
Jd8::flip();
|
||||
std::memcpy(game_frame_, Jd8::getFramebuffer(), sizeof(game_frame_));
|
||||
has_frame_ = true;
|
||||
}
|
||||
|
||||
// Presenta sempre: parteix del frame net del joc, afegeix overlay i envia
|
||||
if (has_frame_) {
|
||||
std::memcpy(presentation_buffer_, game_frame_, sizeof(presentation_buffer_));
|
||||
Screen::get()->present(presentation_buffer_);
|
||||
}
|
||||
|
||||
// Límit de framerate segons VSync.
|
||||
// Nota: quan el runtime posseïx el main loop (SDL_AppIterate /
|
||||
// emscripten), aquest SDL_Delay no és ideal. Fase 7 afegirà un mode
|
||||
// que es basa en el timing intern de SDL en lloc del delay explícit.
|
||||
const Uint32 TARGET_MS = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC;
|
||||
const Uint32 ELAPSED = SDL_GetTicks() - FRAME_START;
|
||||
if (ELAPSED < TARGET_MS) {
|
||||
@@ -285,107 +265,97 @@ void Director::pollAllEvents() {
|
||||
}
|
||||
}
|
||||
|
||||
void Director::handleEvent(const SDL_Event& event) {
|
||||
if (event.type == SDL_EVENT_QUIT) {
|
||||
Jg::quitSignal();
|
||||
requestQuit();
|
||||
}
|
||||
// Hot-plug de gamepad (a Emscripten els dispositius web entren com
|
||||
// JOYSTICK_ADDED/REMOVED perquè SDL no reconeix el GUID)
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED ||
|
||||
event.type == SDL_EVENT_JOYSTICK_ADDED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
|
||||
Gamepad::handleEvent(event);
|
||||
return;
|
||||
}
|
||||
// Empassar-se el KEY_UP de qualsevol tecla que el menú va consumir en KEY_DOWN
|
||||
auto Director::handleMenuEvent(const SDL_Event& event) -> bool {
|
||||
// Empassar-se el KEY_UP d'una tecla que el menú va consumir en KEY_DOWN.
|
||||
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 &&
|
||||
event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) {
|
||||
menu_keys_held_[event.key.scancode] = false;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
// Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot
|
||||
if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
||||
const bool KEY_DOWN = event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat;
|
||||
// Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot.
|
||||
if (Menu::isCapturing() && KEY_DOWN) {
|
||||
Menu::captureKey(event.key.scancode);
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
// Pausa: F11 (o tecla configurada) pausa/reprén la simulació.
|
||||
// No mostrem notificació — l'indicador persistent "Pausa" a la cantonada
|
||||
// superior dreta (pintat per Overlay) ja comunica l'estat.
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
|
||||
event.key.scancode == KeyConfig::scancode("pause_toggle")) {
|
||||
// Pausa / menú toggle.
|
||||
if (KEY_DOWN && event.key.scancode == KeyConfig::scancode("pause_toggle")) {
|
||||
togglePause();
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
// Menú: F12 (o tecla configurada) obre/tanca el menú flotant
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
|
||||
event.key.scancode == KeyConfig::scancode("menu_toggle")) {
|
||||
if (KEY_DOWN && event.key.scancode == KeyConfig::scancode("menu_toggle")) {
|
||||
Menu::toggle();
|
||||
Ji::setInputBlocked(Menu::isOpen());
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
// Si el menú està obert, consumeix tot l'input de teclat
|
||||
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
||||
// Si el menú està obert, consumeix tot l'input de teclat.
|
||||
if (Menu::isOpen() && KEY_DOWN) {
|
||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
Menu::close();
|
||||
Ji::setInputBlocked(false);
|
||||
// Empassa l'ESC fins al release perquè el joc no la veja per polling
|
||||
esc_swallow_until_release_ = true;
|
||||
} else {
|
||||
Menu::handleKey(event.key.scancode);
|
||||
// El menú pot haver-se tancat (p.ex. Backspace al nivell arrel)
|
||||
if (!Menu::isOpen()) {
|
||||
Ji::setInputBlocked(false);
|
||||
}
|
||||
}
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
|
||||
return; // no deixem passar KEY_UP al joc tampoc
|
||||
return true;
|
||||
}
|
||||
// Salta els crèdits amb qualsevol tecla que arribe al joc. Es fa DESPRÉS
|
||||
// del toggle del menú/pausa i del handling del menú obert — així F12 i
|
||||
// SELECT (gamepad) obrin el menú sense cancel·lar els crèdits, i la
|
||||
// navegació per dins del menú tampoc els anul·la.
|
||||
return false;
|
||||
}
|
||||
|
||||
auto Director::handleEscapeEvent(const SDL_Event& event) -> bool {
|
||||
// Salta els crèdits amb qualsevol tecla que arribe al joc.
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
|
||||
Overlay::cancelCredits();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
// Allibera el bloqueig d'ESC quan l'usuari la deixa anar
|
||||
// Allibera el bloqueig d'ESC quan l'usuari la deixa anar.
|
||||
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode == SDL_SCANCODE_ESCAPE && esc_swallow_until_release_) {
|
||||
esc_swallow_until_release_ = false;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
// ESC: interceptem KEY_DOWN per bloquejar-la ABANS que el joc la veja per polling
|
||||
// ESC KEY_DOWN: bloqueja per polling i decideix notificació vs eixida.
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) {
|
||||
esc_blocked_ = true; // Bloqueja ESC per polling immediatament
|
||||
esc_blocked_ = true;
|
||||
if (!Overlay::isEscConsumed()) {
|
||||
// Primera pulsació: mostra notificació
|
||||
Overlay::handleEscape();
|
||||
} else {
|
||||
// Segona pulsació: senyal d'eixida al joc
|
||||
esc_blocked_ = false;
|
||||
key_pressed_ = true;
|
||||
Jg::quitSignal();
|
||||
// Si estem en pausa, la desactivem: el fiber del joc està
|
||||
// congelat i necessita ser reprès per veure la senyal de
|
||||
// quit i poder tornar de forma natural.
|
||||
paused_ = false;
|
||||
}
|
||||
return; // no processa més aquest event
|
||||
return true;
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_UP) {
|
||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
// Ja processat a KEY_DOWN, només deixem netejar el bloqueig
|
||||
// quan l'overlay faça timeout
|
||||
return;
|
||||
} // Comprova si és una tecla d'UI registrada (no passa al joc).
|
||||
// KeyConfig::isGuiKey cobreix totes les tecles GUI a la vegada,
|
||||
// incloent pause_toggle i menu_toggle (defensa en profunditat:
|
||||
// aquestes ja s'haurien hagut de menjar al swallow d'amunt).
|
||||
return false;
|
||||
}
|
||||
|
||||
void Director::handleEvent(const SDL_Event& event) {
|
||||
if (event.type == SDL_EVENT_QUIT) {
|
||||
Jg::quitSignal();
|
||||
requestQuit();
|
||||
}
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED ||
|
||||
event.type == SDL_EVENT_JOYSTICK_ADDED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
|
||||
Gamepad::handleEvent(event);
|
||||
return;
|
||||
}
|
||||
if (handleMenuEvent(event)) {
|
||||
return;
|
||||
}
|
||||
if (handleEscapeEvent(event)) {
|
||||
return;
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode != SDL_SCANCODE_ESCAPE) {
|
||||
const auto SC = event.key.scancode;
|
||||
if (!KeyConfig::isGuiKey(SC)) {
|
||||
key_pressed_ = true;
|
||||
|
||||
@@ -67,6 +67,14 @@ class Director {
|
||||
// Construeix l'escena apropiada segons game_state_ i Info::ctx.
|
||||
// Retorna nullptr si l'state actual no té escena registrada (bug).
|
||||
[[nodiscard]] auto createNextScene() const -> std::unique_ptr<Scenes::Scene>;
|
||||
// Helpers d'iterate() — extrets per reduir complexitat cognitiva.
|
||||
void applyRestart();
|
||||
static void maybeStartTitleCredits();
|
||||
auto tickActiveScene() -> bool; // true = continuar; false = sortir del loop
|
||||
|
||||
// Helpers d'handleEvent() — cada un retorna true si l'event s'ha consumit.
|
||||
auto handleMenuEvent(const SDL_Event& event) -> bool;
|
||||
auto handleEscapeEvent(const SDL_Event& event) -> bool;
|
||||
|
||||
// Buffers persistents entre iteracions. Abans eren locals a run(),
|
||||
// ara són membres perquè iterate() els pot reutilitzar sense tornar-los
|
||||
|
||||
Reference in New Issue
Block a user