diff --git a/source/core/input/gamepad.cpp b/source/core/input/gamepad.cpp index 6e5af71..e7b1677 100644 --- a/source/core/input/gamepad.cpp +++ b/source/core/input/gamepad.cpp @@ -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 diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index c004848..bec764a 100644 --- a/source/core/input/global_inputs.cpp +++ b/source/core/input/global_inputs.cpp @@ -1,6 +1,7 @@ #include "core/input/global_inputs.hpp" #include +#include #include #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& 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; } diff --git a/source/core/rendering/menu.cpp b/source/core/rendering/menu.cpp index ebddb33..006def4 100644 --- a/source/core/rendering/menu.cpp +++ b/source/core/rendering/menu.cpp @@ -60,10 +60,10 @@ namespace Menu { const char* label; ItemKind kind; std::function get_value; // opcional - std::function change; // per TOGGLE/CYCLE/INT_RANGE - std::function enter; // per SUBMENU i ACTION - SDL_Scancode* scancode{nullptr}; // per KEY_BIND - std::function visible; // nullptr ⇒ sempre visible + std::function change; // per TOGGLE/CYCLE/INT_RANGE + std::function enter; // per SUBMENU i ACTION + SDL_Scancode* scancode{nullptr}; // per KEY_BIND + std::function 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(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(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; diff --git a/source/core/rendering/overlay.cpp b/source/core/rendering/overlay.cpp index 498f9e2..ab52091 100644 --- a/source/core/rendering/overlay.cpp +++ b/source/core/rendering/overlay.cpp @@ -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((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((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(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(static_cast(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(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(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(NOW - last_ticks) / 1000.0F; + last_ticks = NOW; - // Calcula delta time - Uint32 now = SDL_GetTicks(); - float dt = static_cast(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((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((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((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(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); } diff --git a/source/core/rendering/text.cpp b/source/core/rendering/text.cpp index 686714c..1de8968 100644 --- a/source/core/rendering/text.cpp +++ b/source/core/rendering/text.cpp @@ -1,5 +1,6 @@ #include "core/rendering/text.hpp" +#include #include #include #include @@ -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; } } diff --git a/source/core/rendering/text.hpp b/source/core/rendering/text.hpp index 1c9c653..3d52f79 100644 --- a/source/core/rendering/text.hpp +++ b/source/core/rendering/text.hpp @@ -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; }; diff --git a/source/core/resources/resource_cache.cpp b/source/core/resources/resource_cache.cpp index d058808..48b05ac 100644 --- a/source/core/resources/resource_cache.cpp +++ b/source/core/resources/resource_cache.cpp @@ -105,6 +105,36 @@ namespace Resource { std::cout << "Resource::Cache: precarregant " << total_count_ << " assets\n"; } + void Cache::stepEachInList(List::Type type, const std::function& clear_fn, LoadStage next, const std::function& 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(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; } diff --git a/source/core/resources/resource_cache.hpp b/source/core/resources/resource_cache.hpp index 28f1a80..78d90a7 100644 --- a/source/core/resources/resource_cache.hpp +++ b/source/core/resources/resource_cache.hpp @@ -4,10 +4,12 @@ #include #include +#include #include #include #include +#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& clear_fn, LoadStage next, const std::function& load_fn); + void stepTextFiles(); std::vector musics_; std::vector sounds_; diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index fda83ee..5b021b6 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -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(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(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; diff --git a/source/core/system/director.hpp b/source/core/system/director.hpp index 870566b..3bb462a 100644 --- a/source/core/system/director.hpp +++ b/source/core/system/director.hpp @@ -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; + // 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 diff --git a/source/external/gif.h b/source/external/gif.h index 0f147d2..ca42ea2 100644 --- a/source/external/gif.h +++ b/source/external/gif.h @@ -1,3 +1,4 @@ +// NOLINTBEGIN(clang-analyzer-unix.Malloc) — codi extern de tercers, no l'auditem #include #include #include @@ -510,3 +511,4 @@ unsigned char* LoadGif(unsigned char *buffer, unsigned short* w, unsigned short* fclose( gif_file ); }*/ +// NOLINTEND(clang-analyzer-unix.Malloc) diff --git a/source/game/bola.cpp b/source/game/bola.cpp index 70f5325..d745f5c 100644 --- a/source/game/bola.cpp +++ b/source/game/bola.cpp @@ -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; } } diff --git a/source/game/bola.hpp b/source/game/bola.hpp index 2f80ca2..bd11599 100644 --- a/source/game/bola.hpp +++ b/source/game/bola.hpp @@ -12,6 +12,6 @@ class Bola : public Sprite { void update(); protected: - Uint8 contador; - Prota* sam; + Uint8 contador_; + Prota* sam_; }; diff --git a/source/game/engendro.cpp b/source/game/engendro.cpp index 74dd5da..a566406 100644 --- a/source/game/engendro.cpp +++ b/source/game/engendro.cpp @@ -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; } diff --git a/source/game/engendro.hpp b/source/game/engendro.hpp index 2319956..a2ae1f8 100644 --- a/source/game/engendro.hpp +++ b/source/game/engendro.hpp @@ -9,5 +9,5 @@ class Engendro : public Sprite { auto update() -> bool; protected: - Uint8 vida; + Uint8 vida_; }; diff --git a/source/game/mapa.cpp b/source/game/mapa.cpp index c0ff215..6e36e99 100644 --- a/source/game/mapa.cpp +++ b/source/game/mapa.cpp @@ -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; } diff --git a/source/game/marcador.cpp b/source/game/marcador.cpp index 09495ba..d707ed8 100644 --- a/source/game/marcador.cpp +++ b/source/game/marcador.cpp @@ -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; diff --git a/source/game/marcador.hpp b/source/game/marcador.hpp index bbb45a1..8b1090a 100644 --- a/source/game/marcador.hpp +++ b/source/game/marcador.hpp @@ -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_; }; diff --git a/source/game/momia.cpp b/source/game/momia.cpp index 4033fc2..e85e29a 100644 --- a/source/game/momia.cpp +++ b/source/game/momia.cpp @@ -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(gfx, this->x, this->y); + this->engendro_ = std::make_unique(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; } diff --git a/source/game/momia.hpp b/source/game/momia.hpp index 9c91954..a039fb8 100644 --- a/source/game/momia.hpp +++ b/source/game/momia.hpp @@ -17,6 +17,13 @@ class Momia : public Sprite { bool dimoni; protected: - Prota* sam; - std::unique_ptr engendro; + Prota* sam_; + std::unique_ptr engendro_; + + void pickDirection(); + void pickHorizontalThenVertical(); + void pickVerticalThenHorizontal(); + void stepInDirection(); + [[nodiscard]] auto collidesWithSam() const -> bool; + void applyCollisionWithSam(); }; diff --git a/source/game/options.cpp b/source/game/options.cpp index 0bfa3b9..4923451 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -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(); + } 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(); + } 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(); - } 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(); - } 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(); - } 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(); - } 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(); - } 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(); - } 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); } } diff --git a/source/game/prota.cpp b/source/game/prota.cpp index 96ccbdd..612a303 100644 --- a/source/game/prota.cpp +++ b/source/game/prota.cpp @@ -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(0 + (i * 5)), + static_cast(1 + (i * 5)), + static_cast(2 + (i * 5)), + static_cast(1 + (i * 5)), + static_cast(0 + (i * 5)), + static_cast(3 + (i * 5)), + static_cast(4 + (i * 5)), + static_cast(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(0 + (i * 5)), - static_cast(1 + (i * 5)), - static_cast(2 + (i * 5)), - static_cast(1 + (i * 5)), - static_cast(0 + (i * 5)), - static_cast(3 + (i * 5)), - static_cast(4 + (i * 5)), - static_cast(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; } diff --git a/source/game/prota.hpp b/source/game/prota.hpp index 8358c85..e8b49bc 100644 --- a/source/game/prota.hpp +++ b/source/game/prota.hpp @@ -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; }; diff --git a/source/game/scenes/intro_new_logo_scene.cpp b/source/game/scenes/intro_new_logo_scene.cpp index a089219..3c85dd6 100644 --- a/source/game/scenes/intro_new_logo_scene.cpp +++ b/source/game/scenes/intro_new_logo_scene.cpp @@ -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(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(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; } diff --git a/source/game/scenes/intro_new_logo_scene.hpp b/source/game/scenes/intro_new_logo_scene.hpp index a4b05c4..bf9a929 100644 --- a/source/game/scenes/intro_new_logo_scene.hpp +++ b/source/game/scenes/intro_new_logo_scene.hpp @@ -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_; diff --git a/source/game/scenes/scene_registry.cpp b/source/game/scenes/scene_registry.cpp index d245169..6a4770a 100644 --- a/source/game/scenes/scene_registry.cpp +++ b/source/game/scenes/scene_registry.cpp @@ -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) { diff --git a/source/game/scenes/slides_scene.cpp b/source/game/scenes/slides_scene.cpp index e9ea5da..165da52 100644 --- a/source/game/scenes/slides_scene.cpp +++ b/source/game/scenes/slides_scene.cpp @@ -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(phase_acc_ms_) / static_cast(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(phase_acc_ms_) / static_cast(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; } diff --git a/source/game/scenes/slides_scene.hpp b/source/game/scenes/slides_scene.hpp index cbecc55..8ab13f9 100644 --- a/source/game/scenes/slides_scene.hpp +++ b/source/game/scenes/slides_scene.hpp @@ -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 diff --git a/source/game/scenes/timeline.cpp b/source/game/scenes/timeline.cpp index 27650e1..60dc084 100644 --- a/source/game/scenes/timeline.cpp +++ b/source/game/scenes/timeline.cpp @@ -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(elapsed_in_step_) / + const float P = static_cast(elapsed_in_step_) / static_cast(std::max(1, s.duration_ms)); - s.continuous(p); + s.continuous(P); } } diff --git a/source/game/sprite.cpp b/source/game/sprite.cpp index f3a83c4..59ce756 100644 --- a/source/game/sprite.cpp +++ b/source/game/sprite.cpp @@ -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); } diff --git a/source/game/sprite.hpp b/source/game/sprite.hpp index fa49365..913f8f8 100644 --- a/source/game/sprite.hpp +++ b/source/game/sprite.hpp @@ -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; };