refactor: extreure helpers per reduir complexitat cognitiva (tidy net)

This commit is contained in:
2026-05-16 16:13:57 +02:00
parent b984e6041e
commit e1bc1b597f
31 changed files with 1145 additions and 1332 deletions
+73 -84
View File
@@ -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
+32 -91
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}
+7
View File
@@ -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;
};
+38 -53
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+8
View File
@@ -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
+2
View File
@@ -1,3 +1,4 @@
// NOLINTBEGIN(clang-analyzer-unix.Malloc) — codi extern de tercers, no l'auditem
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -510,3 +511,4 @@ unsigned char* LoadGif(unsigned char *buffer, unsigned short* w, unsigned short*
fclose( gif_file );
}*/
// NOLINTEND(clang-analyzer-unix.Malloc)
+12 -12
View File
@@ -6,7 +6,7 @@
Bola::Bola(Jd8::Surface gfx, Prota* sam)
: Sprite(gfx) {
this->sam = sam;
this->sam_ = sam;
entitat.frames.reserve(2);
entitat.frames.push_back({30, 155, 15, 15});
@@ -17,28 +17,28 @@ Bola::Bola(Jd8::Surface gfx, Prota* sam)
this->cur_frame = 0;
this->o = 0;
this->cycles_per_frame = 4;
this->cycles_per_frame_ = 4;
this->x = 20;
this->y = 100;
this->contador = 0;
this->contador_ = 0;
}
void Bola::draw() {
if (this->contador == 0) {
if (this->contador_ == 0) {
Sprite::draw();
}
}
void Bola::update() {
if (this->contador == 0) {
if (this->contador_ == 0) {
// Augmentem la x
this->x++;
if (this->x == 280) {
this->contador = 200;
this->contador_ = 200;
}
// Augmentem el frame
if (Jg::getCycleCounter() % this->cycles_per_frame == 0) {
if (Jg::getCycleCounter() % this->cycles_per_frame_ == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
this->cur_frame = 0;
@@ -46,16 +46,16 @@ void Bola::update() {
}
// Comprovem si ha tocat a Sam
if (this->x > (this->sam->x - 7) && this->x < (this->sam->x + 7) && this->y > (this->sam->y - 7) && this->y < (this->sam->y + 7)) {
this->contador = 200;
if (this->x > (this->sam_->x - 7) && this->x < (this->sam_->x + 7) && this->y > (this->sam_->y - 7) && this->y < (this->sam_->y + 7)) {
this->contador_ = 200;
Info::ctx.vida--;
if (Info::ctx.vida == 0) {
this->sam->o = 5;
this->sam_->o = 5;
}
}
} else {
this->contador--;
if (this->contador == 0) {
this->contador_--;
if (this->contador_ == 0) {
this->x = 20;
}
}
+2 -2
View File
@@ -12,6 +12,6 @@ class Bola : public Sprite {
void update();
protected:
Uint8 contador;
Prota* sam;
Uint8 contador_;
Prota* sam_;
};
+5 -5
View File
@@ -22,25 +22,25 @@ Engendro::Engendro(Jd8::Surface gfx, Uint16 x, Uint16 y)
entitat.animacions[0].frames = {0, 1, 2, 3, 2, 1};
this->cur_frame = 0;
this->vida = 18;
this->vida_ = 18;
this->x = x;
this->y = y;
this->o = 0;
this->cycles_per_frame = 30;
this->cycles_per_frame_ = 30;
}
auto Engendro::update() -> bool {
bool mort = false;
if (Jg::getCycleCounter() % this->cycles_per_frame == 0) {
if (Jg::getCycleCounter() % this->cycles_per_frame_ == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
this->cur_frame = 0;
}
this->vida--;
this->vida_--;
}
if (vida == 0) {
if (vida_ == 0) {
mort = true;
}
+1 -1
View File
@@ -9,5 +9,5 @@ class Engendro : public Sprite {
auto update() -> bool;
protected:
Uint8 vida;
Uint8 vida_;
};
+1 -1
View File
@@ -247,7 +247,7 @@ void Mapa::comprovaCaixa(Uint8 num) {
}
// Si algun costat encara no està passat, no hi ha res que fer
if (std::any_of(std::begin(this->tombes[num].costat), std::end(this->tombes[num].costat), [](bool c) { return !c; })) {
if (std::ranges::any_of(this->tombes[num].costat, [](bool c) { return !c; })) {
return;
}
+16 -16
View File
@@ -1,8 +1,8 @@
#include "game/marcador.hpp"
Marcador::Marcador(Jd8::Surface gfx, Prota* sam) {
this->gfx = gfx;
this->sam = sam;
this->gfx_ = gfx;
this->sam_ = sam;
}
void Marcador::draw() {
@@ -15,47 +15,47 @@ void Marcador::draw() {
this->pintaNumero(156, 2, (Info::ctx.diners % 100) / 10);
this->pintaNumero(163, 2, Info::ctx.diners % 10);
if (this->sam->pergami) {
Jd8::blitCK(190, 1, this->gfx, 209, 185, 15, 14, 255);
if (this->sam_->pergami) {
Jd8::blitCK(190, 1, this->gfx_, 209, 185, 15, 14, 255);
}
Jd8::blitCK(271, 1, this->gfx, 0, 20, 15, Info::ctx.vida * 3, 255);
Jd8::blitCK(271, 1, this->gfx_, 0, 20, 15, Info::ctx.vida * 3, 255);
if (Info::ctx.vida < 5) {
Jd8::blitCK(271, 1 + (Info::ctx.vida * 3), this->gfx, 75, 20, 15, 15 - (Info::ctx.vida * 3), 255);
Jd8::blitCK(271, 1 + (Info::ctx.vida * 3), this->gfx_, 75, 20, 15, 15 - (Info::ctx.vida * 3), 255);
}
}
void Marcador::pintaNumero(Uint16 x, Uint16 y, Uint8 num) {
switch (num) {
case 0:
Jd8::blitCK(x, y, this->gfx, 141, 193, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 141, 193, 10, 7, 255);
break;
case 1:
Jd8::blitCK(x, y, this->gfx, 100, 185, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 100, 185, 10, 7, 255);
break;
case 2:
Jd8::blitCK(x, y, this->gfx, 110, 185, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 110, 185, 10, 7, 255);
break;
case 3:
Jd8::blitCK(x, y, this->gfx, 120, 185, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 120, 185, 10, 7, 255);
break;
case 4:
Jd8::blitCK(x, y, this->gfx, 130, 185, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 130, 185, 10, 7, 255);
break;
case 5:
Jd8::blitCK(x, y, this->gfx, 140, 185, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 140, 185, 10, 7, 255);
break;
case 6:
Jd8::blitCK(x, y, this->gfx, 101, 193, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 101, 193, 10, 7, 255);
break;
case 7:
Jd8::blitCK(x, y, this->gfx, 111, 193, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 111, 193, 10, 7, 255);
break;
case 8:
Jd8::blitCK(x, y, this->gfx, 121, 193, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 121, 193, 10, 7, 255);
break;
case 9:
Jd8::blitCK(x, y, this->gfx, 131, 193, 10, 7, 255);
Jd8::blitCK(x, y, this->gfx_, 131, 193, 10, 7, 255);
break;
default:
break;
+2 -2
View File
@@ -14,6 +14,6 @@ class Marcador {
protected:
void pintaNumero(Uint16 x, Uint16 y, Uint8 num);
Jd8::Surface gfx;
Prota* sam;
Jd8::Surface gfx_;
Prota* sam_;
};
+101 -88
View File
@@ -7,7 +7,7 @@
Momia::Momia(Jd8::Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
: Sprite(gfx) {
this->dimoni = dimoni;
this->sam = sam;
this->sam_ = sam;
entitat.frames.reserve(20);
for (int row = 0; row < 4; row++) {
@@ -43,7 +43,7 @@ Momia::Momia(Jd8::Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
this->cur_frame = 0;
this->o = rand() % 4;
this->cycles_per_frame = 4;
this->cycles_per_frame_ = 4;
if (this->dimoni) {
if (x == 0) {
@@ -52,7 +52,7 @@ Momia::Momia(Jd8::Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
this->x = x;
}
if (y == 0) {
if (this->sam->y > 100) {
if (this->sam_->y > 100) {
this->y = 30;
} else {
this->y = 170;
@@ -60,7 +60,7 @@ Momia::Momia(Jd8::Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
} else {
this->y = y;
}
this->engendro = std::make_unique<Engendro>(gfx, this->x, this->y);
this->engendro_ = std::make_unique<Engendro>(gfx, this->x, this->y);
} else {
this->x = x;
this->y = y;
@@ -68,104 +68,117 @@ Momia::Momia(Jd8::Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
}
void Momia::draw() {
if (this->engendro) {
this->engendro->draw();
if (this->engendro_) {
this->engendro_->draw();
} else {
Sprite::draw();
if (Info::ctx.num_piramide == 4) {
if ((Jg::getCycleCounter() % 40) < 20) {
Jd8::blitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255);
Jd8::blitCK(this->x, this->y, this->gfx_, 220, 80, 15, 15, 255);
} else {
Jd8::blitCK(this->x, this->y, this->gfx, 235, 80, 15, 15, 255);
Jd8::blitCK(this->x, this->y, this->gfx_, 235, 80, 15, 15, 255);
}
}
}
}
void Momia::pickHorizontalThenVertical() {
if (this->x > this->sam_->x) {
this->o = 3;
} else if (this->x < this->sam_->x) {
this->o = 2;
} else if (this->y < this->sam_->y) {
this->o = 0;
} else if (this->y > this->sam_->y) {
this->o = 1;
}
}
void Momia::pickVerticalThenHorizontal() {
if (this->y < this->sam_->y) {
this->o = 0;
} else if (this->y > this->sam_->y) {
this->o = 1;
} else if (this->x > this->sam_->x) {
this->o = 3;
} else if (this->x < this->sam_->x) {
this->o = 2;
}
}
void Momia::pickDirection() {
if (!this->dimoni) {
this->o = rand() % 4;
return;
}
if (rand() % 2 == 0) {
pickHorizontalThenVertical();
} else {
pickVerticalThenHorizontal();
}
}
void Momia::stepInDirection() {
switch (this->o) {
case 0:
if (y < 170) { this->y++; }
break;
case 1:
if (y > 30) { this->y--; }
break;
case 2:
if (x < 280) { this->x++; }
break;
case 3:
if (x > 20) { this->x--; }
break;
default:
break;
}
}
auto Momia::collidesWithSam() const -> bool {
return this->x > (this->sam_->x - 7) && this->x < (this->sam_->x + 7) &&
this->y > (this->sam_->y - 7) && this->y < (this->sam_->y + 7);
}
void Momia::applyCollisionWithSam() {
if (this->sam_->pergami) {
this->sam_->pergami = false;
return;
}
Info::ctx.vida--;
if (Info::ctx.vida == 0) {
this->sam_->o = 5;
}
}
auto Momia::update() -> bool {
bool morta = false;
if (this->engendro) {
if (this->engendro->update()) {
this->engendro.reset();
if (this->engendro_) {
if (this->engendro_->update()) {
this->engendro_.reset();
}
return morta;
return false;
}
if (this->sam->o < 4 && (this->dimoni || Info::ctx.num_piramide == 5 || Jg::getCycleCounter() % 2 == 0)) {
if ((this->x - 20) % 65 == 0 && (this->y - 30) % 35 == 0) {
if (this->dimoni) {
if (rand() % 2 == 0) {
if (this->x > this->sam->x) {
this->o = 3;
} else if (this->x < this->sam->x) {
this->o = 2;
} else if (this->y < this->sam->y) {
this->o = 0;
} else if (this->y > this->sam->y) {
this->o = 1;
}
} else {
if (this->y < this->sam->y) {
this->o = 0;
} else if (this->y > this->sam->y) {
this->o = 1;
} else if (this->x > this->sam->x) {
this->o = 3;
} else if (this->x < this->sam->x) {
this->o = 2;
}
}
} else {
this->o = rand() % 4;
}
}
switch (this->o) {
case 0:
if (y < 170) {
this->y++;
}
break;
case 1:
if (y > 30) {
this->y--;
}
break;
case 2:
if (x < 280) {
this->x++;
}
break;
case 3:
if (x > 20) {
this->x--;
}
break;
default:
break;
}
if (Jg::getCycleCounter() % this->cycles_per_frame == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
this->cur_frame = 0;
}
}
if (this->x > (this->sam->x - 7) && this->x < (this->sam->x + 7) && this->y > (this->sam->y - 7) && this->y < (this->sam->y + 7)) {
morta = true;
if (this->sam->pergami) {
this->sam->pergami = false;
} else {
Info::ctx.vida--;
if (Info::ctx.vida == 0) {
this->sam->o = 5;
}
}
const bool SAM_ALIVE = this->sam_->o < 4;
const bool MAY_STEP = this->dimoni || Info::ctx.num_piramide == 5 || Jg::getCycleCounter() % 2 == 0;
if (!SAM_ALIVE || !MAY_STEP) {
return false;
}
if ((this->x - 20) % 65 == 0 && (this->y - 30) % 35 == 0) {
pickDirection();
}
stepInDirection();
if (Jg::getCycleCounter() % this->cycles_per_frame_ == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
this->cur_frame = 0;
}
}
return morta;
if (collidesWithSam()) {
applyCollisionWithSam();
return true;
}
return false;
}
+9 -2
View File
@@ -17,6 +17,13 @@ class Momia : public Sprite {
bool dimoni;
protected:
Prota* sam;
std::unique_ptr<Engendro> engendro;
Prota* sam_;
std::unique_ptr<Engendro> engendro_;
void pickDirection();
void pickHorizontalThenVertical();
void pickVerticalThenHorizontal();
void stepInDirection();
[[nodiscard]] auto collidesWithSam() const -> bool;
void applyCollisionWithSam();
};
+27 -37
View File
@@ -437,7 +437,7 @@ namespace Options {
return true;
}
// --- Helper per a parsejar floats de YAML ---
// --- Helpers per a parsejar camps de YAML ---
static void parseFloatField(const fkyaml::node& node, const std::string& key, float& target) {
if (node.contains(key)) {
try {
@@ -448,6 +448,26 @@ namespace Options {
}
}
static void parseIntField(const fkyaml::node& node, const std::string& key, int& target) {
if (node.contains(key)) {
try {
target = node[key].get_value<int>();
} catch (...) {
// @INTENTIONAL: camp YAML no és int vàlid, mantenim valor per defecte.
}
}
}
static void parseBoolField(const fkyaml::node& node, const std::string& key, bool& target) {
if (node.contains(key)) {
try {
target = node[key].get_value<bool>();
} catch (...) {
// @INTENTIONAL: camp YAML no és bool vàlid, mantenim valor per defecte.
}
}
}
// --- Presets PostFX ---
void setPostFXFile(const std::string& path) { postfx_file_path = path; }
@@ -561,42 +581,12 @@ namespace Options {
parseFloatField(p, "mask_brightness", preset.mask_brightness);
parseFloatField(p, "curvature_x", preset.curvature_x);
parseFloatField(p, "curvature_y", preset.curvature_y);
if (p.contains("mask_type")) {
try {
preset.mask_type = p["mask_type"].get_value<int>();
} catch (...) { /* @INTENTIONAL: camp YAML invàlid, mantenim el valor per defecte. */
}
}
if (p.contains("enable_scanlines")) {
try {
preset.enable_scanlines = p["enable_scanlines"].get_value<bool>();
} catch (...) { /* @INTENTIONAL: camp YAML invàlid, mantenim el valor per defecte. */
}
}
if (p.contains("enable_multisample")) {
try {
preset.enable_multisample = p["enable_multisample"].get_value<bool>();
} catch (...) { /* @INTENTIONAL: camp YAML invàlid, mantenim el valor per defecte. */
}
}
if (p.contains("enable_gamma")) {
try {
preset.enable_gamma = p["enable_gamma"].get_value<bool>();
} catch (...) { /* @INTENTIONAL: camp YAML invàlid, mantenim el valor per defecte. */
}
}
if (p.contains("enable_curvature")) {
try {
preset.enable_curvature = p["enable_curvature"].get_value<bool>();
} catch (...) { /* @INTENTIONAL: camp YAML invàlid, mantenim el valor per defecte. */
}
}
if (p.contains("enable_sharper")) {
try {
preset.enable_sharper = p["enable_sharper"].get_value<bool>();
} catch (...) { /* @INTENTIONAL: camp YAML invàlid, mantenim el valor per defecte. */
}
}
parseIntField(p, "mask_type", preset.mask_type);
parseBoolField(p, "enable_scanlines", preset.enable_scanlines);
parseBoolField(p, "enable_multisample", preset.enable_multisample);
parseBoolField(p, "enable_gamma", preset.enable_gamma);
parseBoolField(p, "enable_curvature", preset.enable_curvature);
parseBoolField(p, "enable_sharper", preset.enable_sharper);
crtpi_presets.push_back(preset);
}
}
+132 -148
View File
@@ -5,84 +5,75 @@
#include "core/jail/jgame.hpp"
#include "core/jail/jinput.hpp"
Prota::Prota(Jd8::Surface gfx)
: Sprite(gfx) {
entitat.frames.reserve(82);
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 5; x++) {
Frame f;
f.w = 15;
f.h = 15;
if (Info::ctx.num_piramide == 4) {
f.h -= 5;
}
f.x = x * 15;
f.y = 20 + (y * 15);
entitat.frames.push_back(f);
}
namespace {
// Atura el frame.h a 10 quan piràmide 4 (sprite "petit" amb pijama de presoner).
auto adjustedHeight(int base_h) -> int {
return (Info::ctx.num_piramide == 4) ? base_h - 5 : base_h;
}
for (int y = 95; y < 185; y += 30) {
for (int x = 60; x < 315; x += 15) {
if (x != 300 || y != 155) {
Frame f;
f.w = 15;
f.h = 30;
if (Info::ctx.num_piramide == 4) {
f.h -= 5;
void addFrameGrid(Entitat& entitat, int x0, int x1, int x_step, int y0, int y1, int y_step, int w, int h, int skip_x = -1, int skip_y = -1) {
for (int yy = y0; yy < y1; yy += y_step) {
for (int xx = x0; xx < x1; xx += x_step) {
if (xx == skip_x && yy == skip_y) {
continue;
}
f.x = x;
f.y = y;
Frame f;
f.w = w;
f.h = adjustedHeight(h);
f.x = xx;
f.y = yy;
entitat.frames.push_back(f);
}
}
}
for (int y = 20; y < 50; y += 15) {
for (int x = 225; x < 315; x += 15) {
Frame f;
f.w = 15;
f.h = 15;
if (Info::ctx.num_piramide == 4) {
f.h -= 5;
}
f.x = x;
f.y = y;
entitat.frames.push_back(f);
void buildProtaFrames(Entitat& entitat) {
entitat.frames.reserve(82);
// Cara/quatre direccions (4×5 sprites de 15×15 a y=20..)
addFrameGrid(entitat, 0, 75, 15, 20, 80, 15, 15, 15);
// Animació de mort (15×30 a y=95..; salta x=300/y=155)
addFrameGrid(entitat, 60, 315, 15, 95, 185, 30, 15, 30, 300, 155);
// Animació de victòria (15×15 a y=20.., x=225..)
addFrameGrid(entitat, 225, 315, 15, 20, 50, 15, 15, 15);
}
void buildProtaAnimations(Entitat& entitat) {
entitat.animacions.resize(6);
for (int i = 0; i < 4; i++) {
entitat.animacions[i].frames = {
static_cast<Uint8>(0 + (i * 5)),
static_cast<Uint8>(1 + (i * 5)),
static_cast<Uint8>(2 + (i * 5)),
static_cast<Uint8>(1 + (i * 5)),
static_cast<Uint8>(0 + (i * 5)),
static_cast<Uint8>(3 + (i * 5)),
static_cast<Uint8>(4 + (i * 5)),
static_cast<Uint8>(3 + (i * 5)),
};
}
entitat.animacions[4].frames.resize(50);
for (int i = 0; i < 50; i++) {
entitat.animacions[4].frames[i] = i + 20;
}
entitat.animacions[5].frames.resize(48);
for (int i = 0; i < 12; i++) {
entitat.animacions[5].frames[i] = i + 70;
}
for (int i = 12; i < 48; i++) {
entitat.animacions[5].frames[i] = 81;
}
}
} // namespace
entitat.animacions.resize(6);
for (int i = 0; i < 4; i++) {
entitat.animacions[i].frames = {
static_cast<Uint8>(0 + (i * 5)),
static_cast<Uint8>(1 + (i * 5)),
static_cast<Uint8>(2 + (i * 5)),
static_cast<Uint8>(1 + (i * 5)),
static_cast<Uint8>(0 + (i * 5)),
static_cast<Uint8>(3 + (i * 5)),
static_cast<Uint8>(4 + (i * 5)),
static_cast<Uint8>(3 + (i * 5)),
};
}
entitat.animacions[4].frames.resize(50);
for (int i = 0; i < 50; i++) {
entitat.animacions[4].frames[i] = i + 20;
}
entitat.animacions[5].frames.resize(48);
for (int i = 0; i < 12; i++) {
entitat.animacions[5].frames[i] = i + 70;
}
for (int i = 12; i < 48; i++) {
entitat.animacions[5].frames[i] = 81;
}
Prota::Prota(Jd8::Surface gfx)
: Sprite(gfx) {
buildProtaFrames(entitat);
buildProtaAnimations(entitat);
cur_frame = 0;
x = 150;
y = 30;
o = 0;
cycles_per_frame = 4;
cycles_per_frame_ = 4;
pergami = false;
frame_pejades = 0;
}
@@ -92,94 +83,87 @@ void Prota::draw() {
if (Info::ctx.num_piramide == 4 && this->o != 4) {
if ((Jg::getCycleCounter() % 40) < 20) {
Jd8::blitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255);
Jd8::blitCK(this->x, this->y, this->gfx_, 220, 80, 15, 15, 255);
} else {
Jd8::blitCK(this->x, this->y, this->gfx, 235, 80, 15, 15, 255);
Jd8::blitCK(this->x, this->y, this->gfx_, 235, 80, 15, 15, 255);
}
}
}
auto Prota::readDirection() -> Uint8 {
Uint8 dir = 4;
if (Ji::keyPressed(SDL_SCANCODE_DOWN)) {
if ((this->x - 20) % 65 == 0) { this->o = 0; }
dir = this->o;
}
if (Ji::keyPressed(SDL_SCANCODE_UP)) {
if ((this->x - 20) % 65 == 0) { this->o = 1; }
dir = this->o;
}
if (Ji::keyPressed(SDL_SCANCODE_RIGHT)) {
if ((this->y - 30) % 35 == 0) { this->o = 2; }
dir = this->o;
}
if (Ji::keyPressed(SDL_SCANCODE_LEFT)) {
if ((this->y - 30) % 35 == 0) { this->o = 3; }
dir = this->o;
}
return dir;
}
void Prota::stepInDirection(Uint8 dir) {
switch (dir) {
case 0:
if (this->y < 170) { this->y++; }
break;
case 1:
if (this->y > 30) { this->y--; }
break;
case 2:
if (this->x < 280) { this->x++; }
break;
case 3:
if (this->x > 20) { this->x--; }
break;
default:
break;
}
}
void Prota::advanceWalkingFrame(Uint8 dir) {
if (dir == 4) {
this->cur_frame = 0;
return;
}
this->frame_pejades++;
if (this->frame_pejades == 15) {
this->frame_pejades = 0;
}
if (Jg::getCycleCounter() % this->cycles_per_frame_ == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
this->cur_frame = 0;
}
}
}
auto Prota::advanceFinalAnimation() -> Uint8 {
if (Jg::getCycleCounter() % this->cycles_per_frame_ != 0) {
return 0;
}
this->cur_frame++;
if (this->cur_frame != entitat.animacions[this->o].frames.size()) {
return 0;
}
return (this->o == 4) ? 1 : 2;
}
auto Prota::update() -> Uint8 {
Uint8 eixir = 0;
if (this->o < 4) {
Uint8 dir = 4;
if (Ji::keyPressed(SDL_SCANCODE_DOWN)) {
if ((this->x - 20) % 65 == 0) {
this->o = 0;
}
dir = this->o;
}
if (Ji::keyPressed(SDL_SCANCODE_UP)) {
if ((this->x - 20) % 65 == 0) {
this->o = 1;
}
dir = this->o;
}
if (Ji::keyPressed(SDL_SCANCODE_RIGHT)) {
if ((this->y - 30) % 35 == 0) {
this->o = 2;
}
dir = this->o;
}
if (Ji::keyPressed(SDL_SCANCODE_LEFT)) {
if ((this->y - 30) % 35 == 0) {
this->o = 3;
}
dir = this->o;
}
switch (dir) {
case 0:
if (this->y < 170) {
this->y++;
}
break;
case 1:
if (this->y > 30) {
this->y--;
}
break;
case 2:
if (this->x < 280) {
this->x++;
}
break;
case 3:
if (this->x > 20) {
this->x--;
}
break;
default:
break;
}
if (dir == 4) {
this->cur_frame = 0;
} else {
this->frame_pejades++;
if (this->frame_pejades == 15) {
this->frame_pejades = 0;
}
if (Jg::getCycleCounter() % this->cycles_per_frame == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
this->cur_frame = 0;
}
}
}
eixir = 0U;
} else {
if (Jg::getCycleCounter() % this->cycles_per_frame == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
if (this->o == 4) {
eixir = 1;
} else {
eixir = 2;
}
}
}
if (this->o >= 4) {
return advanceFinalAnimation();
}
return eixir;
const Uint8 DIR = readDirection();
stepInDirection(DIR);
advanceWalkingFrame(DIR);
return 0;
}
+4
View File
@@ -14,4 +14,8 @@ class Prota : public Sprite {
bool pergami;
protected:
auto readDirection() -> Uint8;
void stepInDirection(Uint8 dir);
void advanceWalkingFrame(Uint8 dir);
auto advanceFinalAnimation() -> Uint8;
};
+48 -53
View File
@@ -132,17 +132,59 @@ namespace Scenes {
}
}
void IntroNewLogoScene::advanceRevealing(int delta_ms) {
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ < REVEAL_FRAME_MS) {
return;
}
phase_acc_ms_ = 0;
reveal_cursor_visible_ = !reveal_cursor_visible_;
if (!reveal_cursor_visible_) {
return;
}
++reveal_letter_;
if (reveal_letter_ >= 9) {
phase_ = Phase::FULL_LOGO_FLASH;
reveal_letter_ = 8;
}
}
void IntroNewLogoScene::advancePaletteStep(int delta_ms) {
phase_acc_ms_ += delta_ms;
while (phase_acc_ms_ >= PALETTE_CYCLE_STEP_MS && palette_step_ < PALETTE_CYCLE_STEPS) {
phase_acc_ms_ -= PALETTE_CYCLE_STEP_MS;
advancePaletteCycle();
++palette_step_;
}
render();
if (palette_step_ >= PALETTE_CYCLE_STEPS) {
phase_ = Phase::FINAL_WAIT;
phase_acc_ms_ = 0;
}
}
void IntroNewLogoScene::advanceSpritesPhase(int delta_ms) {
if (!sprites_scene_) {
sprites_scene_ = std::make_unique<IntroSpritesScene>(std::move(gfx_));
sprites_scene_->onEnter();
}
sprites_scene_->tick(delta_ms);
if (sprites_scene_->done()) {
Info::ctx.num_piramide = 0;
phase_ = Phase::DONE;
}
}
void IntroNewLogoScene::tick(int delta_ms) {
// Qualsevol tecla durant el revelat o el ciclo de paleta salta
// TOTA la intro (inclou saltar la fase de sprites). Durant SPRITES
// deixem que la sub-escena gestione el seu propi skip (que a més
// respecta la fase "final" no skippable de la variant 0).
// deixem que la sub-escena gestione el seu propi skip.
if (phase_ != Phase::SPRITES && phase_ != Phase::DONE && Ji::anyKey()) {
Info::ctx.num_piramide = 0;
phase_ = Phase::DONE;
return;
}
switch (phase_) {
case Phase::INITIAL:
phase_acc_ms_ += delta_ms;
@@ -152,25 +194,9 @@ namespace Scenes {
phase_acc_ms_ = 0;
}
break;
case Phase::REVEALING:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= REVEAL_FRAME_MS) {
phase_acc_ms_ = 0;
reveal_cursor_visible_ = !reveal_cursor_visible_;
// Quan acabem els dos frames d'una lletra (cursor on → off),
// passem a la següent lletra.
if (reveal_cursor_visible_) {
++reveal_letter_;
if (reveal_letter_ >= 9) {
phase_ = Phase::FULL_LOGO_FLASH;
reveal_letter_ = 8;
}
}
}
advanceRevealing(delta_ms);
break;
case Phase::FULL_LOGO_FLASH:
phase_acc_ms_ += delta_ms;
render();
@@ -179,24 +205,9 @@ namespace Scenes {
phase_acc_ms_ = 0;
}
break;
case Phase::PALETTE_CYCLE:
phase_acc_ms_ += delta_ms;
// Avancem passos de paleta cada 20 ms. Si el delta és gran,
// consumim múltiples passos en la mateixa crida.
while (phase_acc_ms_ >= PALETTE_CYCLE_STEP_MS &&
palette_step_ < PALETTE_CYCLE_STEPS) {
phase_acc_ms_ -= PALETTE_CYCLE_STEP_MS;
advancePaletteCycle();
++palette_step_;
}
render();
if (palette_step_ >= PALETTE_CYCLE_STEPS) {
phase_ = Phase::FINAL_WAIT;
phase_acc_ms_ = 0;
}
advancePaletteStep(delta_ms);
break;
case Phase::FINAL_WAIT:
phase_acc_ms_ += delta_ms;
render();
@@ -204,25 +215,9 @@ namespace Scenes {
phase_ = Phase::SPRITES;
}
break;
case Phase::SPRITES:
// Sub-escena construïda al primer tick. Transferim el gfx_
// per move — la sub-escena se n'ocupa fins que es destruix.
// Cada tick successiu delega l'animació dels sprites.
if (!sprites_scene_) {
sprites_scene_ = std::make_unique<IntroSpritesScene>(std::move(gfx_));
sprites_scene_->onEnter();
}
sprites_scene_->tick(delta_ms);
if (sprites_scene_->done()) {
// El vell `Go()` post-switch feia `num_piramide = 0`
// per passar al menú. Sense açò el while del fiber
// tornaria a crear IntroNewLogoScene infinitament.
Info::ctx.num_piramide = 0;
phase_ = Phase::DONE;
}
advanceSpritesPhase(delta_ms);
break;
case Phase::DONE:
break;
}
+9 -5
View File
@@ -41,17 +41,21 @@ namespace Scenes {
private:
enum class Phase : std::uint8_t {
INITIAL, // pantalla negra 1000 ms
REVEALING, // 9 × 2 frames × 150 ms cada un
INITIAL, // pantalla negra 1000 ms
REVEALING, // 9 × 2 frames × 150 ms cada un
FULL_LOGO_FLASH, // logo complet + cursor, 200 ms
PALETTE_CYCLE, // 256 passos × 20 ms modificant paleta
FINAL_WAIT, // 20 ms final
SPRITES, // tick delegat a IntroSpritesScene fins que acaba
PALETTE_CYCLE, // 256 passos × 20 ms modificant paleta
FINAL_WAIT, // 20 ms final
SPRITES, // tick delegat a IntroSpritesScene fins que acaba
DONE,
};
void render();
void advancePaletteCycle();
// Helpers per a `tick()` — extrets per reduir complexitat cognitiva.
void advanceRevealing(int delta_ms);
void advancePaletteStep(int delta_ms);
void advanceSpritesPhase(int delta_ms);
SurfaceHandle gfx_;
SurfaceHandle cursor_surf_;
+2 -2
View File
@@ -3,8 +3,8 @@
namespace Scenes {
auto SceneRegistry::instance() -> SceneRegistry& {
static SceneRegistry inst;
return inst;
static SceneRegistry instance_;
return instance_;
}
void SceneRegistry::registerScene(int state_key, Factory factory) {
+82 -75
View File
@@ -65,12 +65,12 @@ namespace Scenes {
next_state_ = 0;
}
void SlidesScene::drawSlide(int slide_idx, int POS_X) {
void SlidesScene::drawSlide(int slide_idx, int pos_x) {
const int SRC_Y = slide_idx * SLIDE_H;
// Clipping manual: translada un rect de 320×65 des de (POS_X, SLIDE_Y)
// a l'àrea visible (0..319, SLIDE_Y..SLIDE_Y+64).
int dst_x = POS_X;
int dst_x = pos_x;
int src_x = 0;
int w = 320;
@@ -99,99 +99,106 @@ namespace Scenes {
phase_ = Phase::FADE_FINAL;
}
void SlidesScene::tick(int delta_ms) {
// Skip: qualsevol tecla salta directament al fade final. Per fidelitat
// al vell doSlides, el skip NO atura la música explícitament — només
// el final natural crida Ja::fadeOutMusic (beginFinalFade() distingeix).
if (!skip_triggered_ && Ji::anyKey()) {
skip_triggered_ = true;
if (num_piramide_at_start_ != 7) {
Audio::get()->fadeOutMusic(250);
}
fade_.startFadeOut();
phase_ = Phase::FADE_FINAL;
void SlidesScene::triggerSkip() {
skip_triggered_ = true;
if (num_piramide_at_start_ != 7) {
Audio::get()->fadeOutMusic(250);
}
fade_.startFadeOut();
phase_ = Phase::FADE_FINAL;
}
void SlidesScene::tickSlideEnter(int delta_ms) {
phase_acc_ms_ += delta_ms;
int slide_idx = 2;
if (phase_ == Phase::SLIDE1_ENTER) {
slide_idx = 0;
} else if (phase_ == Phase::SLIDE2_ENTER) {
slide_idx = 1;
}
const float T = std::min(1.0F, static_cast<float>(phase_acc_ms_) / static_cast<float>(SCROLL_MS));
const float EASED = Easing::outCubic(T);
drawSlide(slide_idx, Easing::lerpInt(SLIDE_START_X[slide_idx], 0, EASED));
if (phase_acc_ms_ < SCROLL_MS) {
return;
}
drawSlide(slide_idx, 0);
switch (phase_) {
case Phase::SLIDE1_ENTER:
phase_ = Phase::SLIDE1_HOLD;
break;
case Phase::SLIDE2_ENTER:
phase_ = Phase::SLIDE2_HOLD;
break;
default:
phase_ = Phase::SLIDE3_HOLD;
break;
}
phase_acc_ms_ = 0;
}
void SlidesScene::tickHoldIntermediate(int delta_ms) {
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ < HOLD_MS) {
return;
}
fade_.startFadeOut();
phase_ = (phase_ == Phase::SLIDE1_HOLD) ? Phase::FADE_OUT1 : Phase::FADE_OUT2;
phase_acc_ms_ = 0;
}
void SlidesScene::tickFadeOutIntermediate(int delta_ms) {
fade_.tick(delta_ms);
if (!fade_.done()) {
return;
}
restorePalette();
Jd8::clearScreen(BG_COLOR_INDEX);
phase_ = (phase_ == Phase::FADE_OUT1) ? Phase::SLIDE2_ENTER : Phase::SLIDE3_ENTER;
phase_acc_ms_ = 0;
}
void SlidesScene::tickFinalFade(int delta_ms) {
fade_.tick(delta_ms);
if (!fade_.done()) {
return;
}
if (num_piramide_at_start_ == 7) {
Info::ctx.num_piramide = 8;
next_state_ = 1;
} else {
next_state_ = 0;
}
phase_ = Phase::DONE;
}
void SlidesScene::tick(int delta_ms) {
if (!skip_triggered_ && Ji::anyKey()) {
triggerSkip();
}
switch (phase_) {
case Phase::SLIDE1_ENTER:
case Phase::SLIDE2_ENTER:
case Phase::SLIDE3_ENTER: {
phase_acc_ms_ += delta_ms;
int slide_idx = 2;
if (phase_ == Phase::SLIDE1_ENTER) {
slide_idx = 0;
} else if (phase_ == Phase::SLIDE2_ENTER) {
slide_idx = 1;
}
const float T = std::min(1.0F, static_cast<float>(phase_acc_ms_) / static_cast<float>(SCROLL_MS));
const float EASED = Easing::outCubic(T);
const int POS_X = Easing::lerpInt(SLIDE_START_X[slide_idx], 0, EASED);
drawSlide(slide_idx, POS_X);
if (phase_acc_ms_ >= SCROLL_MS) {
// Garanteix posició final exacta (POS_X=0).
drawSlide(slide_idx, 0);
if (phase_ == Phase::SLIDE1_ENTER) {
phase_ = Phase::SLIDE1_HOLD;
} else if (phase_ == Phase::SLIDE2_ENTER) {
phase_ = Phase::SLIDE2_HOLD;
} else {
phase_ = Phase::SLIDE3_HOLD;
}
phase_acc_ms_ = 0;
}
case Phase::SLIDE3_ENTER:
tickSlideEnter(delta_ms);
break;
}
case Phase::SLIDE1_HOLD:
case Phase::SLIDE2_HOLD:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= HOLD_MS) {
fade_.startFadeOut();
if (phase_ == Phase::SLIDE1_HOLD) {
phase_ = Phase::FADE_OUT1;
} else {
phase_ = Phase::FADE_OUT2;
}
phase_acc_ms_ = 0;
}
tickHoldIntermediate(delta_ms);
break;
case Phase::SLIDE3_HOLD:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= HOLD_MS) {
beginFinalFade();
}
break;
case Phase::FADE_OUT1:
case Phase::FADE_OUT2:
fade_.tick(delta_ms);
if (fade_.done()) {
restorePalette();
Jd8::clearScreen(BG_COLOR_INDEX);
if (phase_ == Phase::FADE_OUT1) {
phase_ = Phase::SLIDE2_ENTER;
} else {
phase_ = Phase::SLIDE3_ENTER;
}
phase_acc_ms_ = 0;
}
tickFadeOutIntermediate(delta_ms);
break;
case Phase::FADE_FINAL:
fade_.tick(delta_ms);
if (fade_.done()) {
if (num_piramide_at_start_ == 7) {
Info::ctx.num_piramide = 8;
next_state_ = 1;
} else {
next_state_ = 0;
}
phase_ = Phase::DONE;
}
tickFinalFade(delta_ms);
break;
case Phase::DONE:
break;
}
+6
View File
@@ -65,6 +65,12 @@ namespace Scenes {
void drawSlide(int slide_idx, int pos_x);
void restorePalette();
void beginFinalFade();
// Helpers per a `tick()` — extrets per reduir complexitat cognitiva.
void triggerSkip();
void tickSlideEnter(int delta_ms);
void tickHoldIntermediate(int delta_ms);
void tickFadeOutIntermediate(int delta_ms);
void tickFinalFade(int delta_ms);
SurfaceHandle gfx_;
Jd8::Palette pal_aux_{nullptr}; // còpia "neta" que preservem
+2 -2
View File
@@ -63,9 +63,9 @@ namespace Scenes {
// Pot ser que el següent pas siga una cadena de one-shots.
flushOneShots();
} else if (s.continuous) {
const float p = static_cast<float>(elapsed_in_step_) /
const float P = static_cast<float>(elapsed_in_step_) /
static_cast<float>(std::max(1, s.duration_ms));
s.continuous(p);
s.continuous(P);
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
#include "game/sprite.hpp"
Sprite::Sprite(Jd8::Surface gfx)
: gfx(gfx) {}
: gfx_(gfx) {}
void Sprite::draw() {
const Frame& f = entitat.frames[entitat.animacions[o].frames[cur_frame]];
Jd8::blitCK(x, y, gfx, f.x, f.y, f.w, f.h, 255);
Jd8::blitCK(x, y, gfx_, f.x, f.y, f.w, f.h, 255);
}
+2 -2
View File
@@ -34,6 +34,6 @@ class Sprite {
Uint16 o = 0;
protected:
Jd8::Surface gfx;
Uint8 cycles_per_frame = 1;
Jd8::Surface gfx_;
Uint8 cycles_per_frame_ = 1;
};