diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index 8fbbda7..41971e5 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -71,3 +71,9 @@ notifications: filter_nearest: "FILTRE: NEAREST" pause: "PAUSA" resume: "REPRES" + +credits: + port_role: "Conversio a C++ i SDL3" + port_name: "JailDoctor" + modern_role: "Opcions, menu, overlay i shaders" + modern_name: "JailDesigner" diff --git a/source/core/rendering/overlay.cpp b/source/core/rendering/overlay.cpp index 38d569d..e83d4a9 100644 --- a/source/core/rendering/overlay.cpp +++ b/source/core/rendering/overlay.cpp @@ -39,7 +39,11 @@ namespace Overlay { FINISHED }; struct Notification { - std::string message; + std::vector lines; // una o més línies de text + Overlay::NotifPosition pos{Overlay::NotifPosition::TOP_LEFT_SLIDE}; + Overlay::NotifStyle style{Overlay::NotifStyle::BOX}; + Uint32 accent_color{NOTIF_BG_COLOR}; // fons (BOX) o ombra (SHADOW) + Uint32 text_color{NOTIF_TEXT_COLOR}; Status status{Status::RISING}; float anim{0.0F}; // 0 = fora de pantalla, 1 = posició final float timer{0.0F}; @@ -67,6 +71,21 @@ namespace Overlay { }; static InfoSegment info_segments_[INFO_SEGMENT_COUNT]; + // --- Crèdits cinematogràfics --- + // Usen el sistema de notificacions en posició TOP_CENTER_DROP. + enum class CreditsPhase { IDLE, + DELAY, + PLAYING_1, + GAP, + PLAYING_2 }; + static CreditsPhase credits_phase_ = CreditsPhase::IDLE; + static float credits_timer_ = 0.0F; // segons dins la phase actual + static constexpr float CREDITS_DELAY = 2.0F; + static constexpr float CREDITS_GAP = 0.4F; + static constexpr float CREDITS_HOLD = 7.5F; + static constexpr Uint32 CREDITS_BG = NOTIF_BG_COLOR; // mateix marró de les notificacions + static constexpr Uint32 CREDITS_FG = NOTIF_TEXT_COLOR; // mateix cian + // --- Doble ESC per a eixir --- static bool esc_waiting_ = false; @@ -131,17 +150,49 @@ namespace Overlay { if (notif.status == Status::FINISHED) continue; - // Posició: entra des de l'esquerra - int target_x = NOTIF_MARGIN_X; - int target_y = NOTIF_MARGIN_Y; - int box_x = target_x - static_cast((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X)); - int box_y = target_y; + // 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 de la caixa - drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, NOTIF_BG_COLOR); + // 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 text dins la caixa - font_->draw(pixel_data, box_x + NOTIF_PADDING_H, box_y + NOTIF_PADDING_V, notif.message.c_str(), NOTIF_TEXT_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; + } } // Render info (FPS, driver, shader) — animat amb slide vertical @@ -247,12 +298,69 @@ namespace Overlay { int w = font_->width(pause_text); int x = SCREEN_W - w - 4; int y = 4; - // Ombra - font_->draw(pixel_data, x + 1, y + 1, pause_text, 0xFF000000); + // 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); } + // 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 + notifications_.erase( + std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }), + notifications_.end()); + // Menú flotant per damunt de tot if (Menu::isOpen()) { Menu::render(pixel_data); @@ -260,14 +368,36 @@ namespace Overlay { } void showNotification(const char* text, float duration_seconds) { + showNotification({std::string(text)}, duration_seconds, NotifPosition::TOP_LEFT_SLIDE, NotifStyle::BOX, NOTIF_BG_COLOR, NOTIF_TEXT_COLOR); + } + + void showNotification(const std::vector& lines, + float duration_seconds, + NotifPosition pos, + NotifStyle style, + Uint32 accent_color, + Uint32 text_color) { // Reemplaça la notificació anterior notifications_.clear(); Notification notif; - notif.message = text; + notif.lines = lines; + notif.pos = pos; + notif.style = style; + notif.accent_color = accent_color; + notif.text_color = text_color; notif.duration = duration_seconds; - notif.box_w = font_->width(text) + NOTIF_PADDING_H * 2; - notif.box_h = font_->charHeight() + NOTIF_PADDING_V * 2; + + // Calcula l'amplada màxima de les línies + int max_w = 0; + for (const auto& line : lines) { + int w = font_->width(line.c_str()); + if (w > max_w) max_w = w; + } + notif.box_w = max_w + NOTIF_PADDING_H * 2; + int line_h = font_->charHeight(); + int line_count = static_cast(lines.size()); + notif.box_h = line_count * line_h + (line_count - 1) * 1 + NOTIF_PADDING_V * 2; notifications_.push_back(notif); } @@ -294,6 +424,22 @@ namespace Overlay { } } + void startCredits() { + if (credits_phase_ != CreditsPhase::IDLE) return; + credits_phase_ = CreditsPhase::DELAY; + credits_timer_ = 0.0F; + } + + void cancelCredits() { + credits_phase_ = CreditsPhase::IDLE; + credits_timer_ = 0.0F; + notifications_.clear(); + } + + auto creditsActive() -> bool { + return credits_phase_ != CreditsPhase::IDLE; + } + auto isEscConsumed() -> bool { return esc_waiting_; } diff --git a/source/core/rendering/overlay.hpp b/source/core/rendering/overlay.hpp index 12fbbf7..12f8383 100644 --- a/source/core/rendering/overlay.hpp +++ b/source/core/rendering/overlay.hpp @@ -2,6 +2,9 @@ #include +#include +#include + namespace Overlay { void init(); void destroy(); @@ -9,9 +12,31 @@ namespace Overlay { // Pinta l'overlay sobre el buffer ARGB — cridar abans de presentar void render(Uint32* pixel_data); - // Mostra una notificació amb animació slide-in/stay/slide-out + // Posició + animació d'una notificació + enum class NotifPosition { + TOP_LEFT_SLIDE, // Cantó superior esquerra, slide horizontal des de fora + TOP_CENTER_DROP, // Centrat horitzontal, baixa des de sobre + }; + + // Estil de la notificació: caixa de fons, ombra o contorn del text + enum class NotifStyle { + BOX, // Rectangle de fons amb accent_color + SHADOW, // Sense fons, text amb ombra (offset +1,+1) en accent_color + OUTLINE, // Sense fons, text amb contorn 4-direccional en accent_color + }; + + // Mostra una notificació amb animació slide-in/stay/slide-out (API simple) void showNotification(const char* text, float duration_seconds = 2.0F); + // Versió extensa: múltiples línies, posició, estil, colors i durada personalitzats. + // accent_color s'usa com a fons (si style=BOX) o com a ombra (si style=SHADOW). + void showNotification(const std::vector& lines, + float duration_seconds, + NotifPosition pos, + NotifStyle style, + Uint32 accent_color, + Uint32 text_color); + // Activa/desactiva la info de renderitzat (FPS, driver, shader, preset) void toggleRenderInfo(); void cycleRenderInfo(int dir); // dir=+1 avant, -1 endarrere @@ -20,6 +45,11 @@ namespace Overlay { // `mono_mask` és un bitfield: bit N = 1 → segment N amb amplada monoespaiada. void setRenderInfoSegments(const char* s0, const char* s1, const char* s2, const char* s3, unsigned int mono_mask = 0); + // Crèdits seqüencials cinematogràfics (2 blocs fade-in/hold/fade-out) + void startCredits(); + void cancelCredits(); + auto creditsActive() -> bool; + // Gestió d'eixida amb doble ESC // Retorna true si l'ESC ha sigut consumit (no s'ha de passar al joc) auto handleEscape() -> bool; diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index c0ea91e..1052911 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -71,6 +71,15 @@ void Director::run() { GlobalInputs::handle(); Mouse::updateCursorVisibility(); + // Dispara els crèdits cinematogràfics la primera vegada que el joc + // arriba al menú del títol (info::num_piramide == 0). Lectura no + // atòmica d'un int global: race benigna, tard d'1 frame en el pitjor cas. + static bool credits_triggered = false; + if (!credits_triggered && info::num_piramide == 0) { + Overlay::startCredits(); + credits_triggered = true; + } + // Si l'overlay ja no bloqueja ESC (timeout), desbloquegem if (esc_blocked_ && !Overlay::isEscConsumed()) { esc_blocked_ = false; @@ -133,6 +142,11 @@ void Director::handleEvents() { Gamepad::handleEvent(event); continue; } + // Salta els crèdits amb qualsevol tecla; no deixem que arribi al joc + if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) { + Overlay::cancelCredits(); + continue; + } // Empassar-se el KEY_UP de qualsevol 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]) {