460 lines
19 KiB
C++
460 lines
19 KiB
C++
#include "core/rendering/overlay.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "core/locale/locale.hpp"
|
|
#include "core/rendering/menu.hpp"
|
|
#include "core/rendering/text.hpp"
|
|
#include "core/system/director.hpp"
|
|
#include "game/options.hpp"
|
|
#include "utils/easing.hpp"
|
|
|
|
namespace Overlay {
|
|
|
|
static std::unique_ptr<Text> font_;
|
|
|
|
// --- Aspecte de la notificació ---
|
|
static constexpr Uint32 NOTIF_BG_COLOR = 0xFF2E1A1A; // Fons blau fosc (ABGR)
|
|
static constexpr Uint32 NOTIF_TEXT_COLOR = 0xFFFFFF00; // Text cyan (ABGR)
|
|
static constexpr int NOTIF_PADDING_H = 8; // Padding horitzontal (esquerra/dreta dins la caixa)
|
|
static constexpr int NOTIF_PADDING_V = 4; // Padding vertical (dalt/baix dins la caixa)
|
|
static constexpr int NOTIF_MARGIN_X = 4; // Offset des de la vora esquerra de la pantalla
|
|
static constexpr int NOTIF_MARGIN_Y = 4; // Offset des de la vora superior de la pantalla
|
|
|
|
// --- Animació ---
|
|
static constexpr float SLIDE_SPEED = 4.0F; // Velocitat de l'animació (unitats/segon)
|
|
|
|
// --- Pantalla ---
|
|
static constexpr int SCREEN_W = 320;
|
|
static constexpr int SCREEN_H = 200;
|
|
|
|
// --- Estat de les notificacions ---
|
|
|
|
enum class Status { RISING,
|
|
STAY,
|
|
VANISHING,
|
|
FINISHED };
|
|
|
|
struct Notification {
|
|
std::vector<std::string> 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};
|
|
float duration{2.0F};
|
|
int box_w{0}; // Ample de la caixa (calculat al crear)
|
|
int box_h{0}; // Alçada de la caixa (calculat al crear)
|
|
};
|
|
|
|
static std::vector<Notification> notifications_;
|
|
static Uint32 last_ticks_ = 0;
|
|
|
|
// --- Render info ---
|
|
static Options::RenderInfoPosition info_visible_pos_ = Options::RenderInfoPosition::OFF;
|
|
static float info_anim_ = 0.0F; // 0 = fora de pantalla, 1 = posició final
|
|
static constexpr float INFO_SLIDE_SPEED = 5.0F;
|
|
|
|
// Segments del render info — cadascú amb la seva pròpia visibilitat animada
|
|
static constexpr int INFO_SEGMENT_COUNT = 4;
|
|
static constexpr float SEG_SPEED = 6.0F; // ~165 ms per aparèixer/desaparèixer
|
|
struct InfoSegment {
|
|
std::string text;
|
|
float anim{0.0F};
|
|
bool visible{false};
|
|
bool mono_digits{false}; // si true, dígits amb amplada fixa (la resta natural)
|
|
};
|
|
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;
|
|
|
|
void init() {
|
|
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
|
|
last_ticks_ = SDL_GetTicks();
|
|
}
|
|
|
|
void destroy() {
|
|
font_.reset();
|
|
notifications_.clear();
|
|
}
|
|
|
|
// Pinta un rectangle sòlid dins els límits de la pantalla
|
|
static void drawRect(Uint32* pixel_data, int rx, int ry, int rw, int rh, Uint32 color) {
|
|
for (int row = ry; row < ry + rh; row++) {
|
|
if (row < 0 || row >= SCREEN_H) continue;
|
|
for (int col = rx; col < rx + rw; col++) {
|
|
if (col < 0 || col >= SCREEN_W) continue;
|
|
pixel_data[col + row * SCREEN_W] = color;
|
|
}
|
|
}
|
|
}
|
|
|
|
void render(Uint32* pixel_data) {
|
|
if (!font_ || !pixel_data) return;
|
|
|
|
// Calcula delta time
|
|
Uint32 now = SDL_GetTicks();
|
|
float dt = static_cast<float>(now - last_ticks_) / 1000.0F;
|
|
last_ticks_ = now;
|
|
|
|
// Actualitza i pinta cada notificació
|
|
for (auto& notif : notifications_) {
|
|
switch (notif.status) {
|
|
case Status::RISING:
|
|
notif.anim += SLIDE_SPEED * dt;
|
|
if (notif.anim >= 1.0F) {
|
|
notif.anim = 1.0F;
|
|
notif.status = Status::STAY;
|
|
notif.timer = 0.0F;
|
|
}
|
|
break;
|
|
|
|
case Status::STAY:
|
|
notif.timer += dt;
|
|
if (notif.timer >= notif.duration) {
|
|
notif.status = Status::VANISHING;
|
|
}
|
|
break;
|
|
|
|
case Status::VANISHING:
|
|
notif.anim -= SLIDE_SPEED * dt;
|
|
if (notif.anim <= 0.0F) {
|
|
notif.status = Status::FINISHED;
|
|
}
|
|
break;
|
|
|
|
case Status::FINISHED:
|
|
break;
|
|
}
|
|
|
|
if (notif.status == Status::FINISHED) continue;
|
|
|
|
// Posició segons el tipus
|
|
int box_x = 0;
|
|
int box_y = 0;
|
|
switch (notif.pos) {
|
|
case NotifPosition::TOP_LEFT_SLIDE: {
|
|
int target_x = NOTIF_MARGIN_X;
|
|
int target_y = NOTIF_MARGIN_Y;
|
|
box_x = target_x - static_cast<int>((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X));
|
|
box_y = target_y;
|
|
break;
|
|
}
|
|
case NotifPosition::TOP_CENTER_DROP: {
|
|
int target_y = NOTIF_MARGIN_Y;
|
|
box_x = (SCREEN_W - notif.box_w) / 2;
|
|
// Baixa des de sobre de la pantalla fins a target_y
|
|
box_y = target_y - static_cast<int>((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y));
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Pinta fons (si BOX)
|
|
if (notif.style == NotifStyle::BOX) {
|
|
drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color);
|
|
}
|
|
|
|
// Pinta el text línia a línia (amb ombra o contorn segons style)
|
|
int line_h = font_->charHeight();
|
|
int line_y = box_y + NOTIF_PADDING_V;
|
|
for (const auto& line : notif.lines) {
|
|
int line_w = font_->width(line.c_str());
|
|
int line_x = box_x + (notif.box_w - line_w) / 2; // centrat dins la caixa
|
|
if (notif.style == NotifStyle::SHADOW) {
|
|
font_->draw(pixel_data, line_x + 1, line_y + 1, line.c_str(), notif.accent_color);
|
|
} else if (notif.style == NotifStyle::OUTLINE) {
|
|
// Contorn 4-direccional (N, S, E, W)
|
|
font_->draw(pixel_data, line_x, line_y - 1, line.c_str(), notif.accent_color);
|
|
font_->draw(pixel_data, line_x, line_y + 1, line.c_str(), notif.accent_color);
|
|
font_->draw(pixel_data, line_x - 1, line_y, line.c_str(), notif.accent_color);
|
|
font_->draw(pixel_data, line_x + 1, line_y, line.c_str(), notif.accent_color);
|
|
}
|
|
font_->draw(pixel_data, line_x, line_y, line.c_str(), notif.text_color);
|
|
line_y += line_h + 1;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
if (info_anim_ > 1.0F) 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
if (seg.anim > target) seg.anim = target;
|
|
} else if (seg.anim > target) {
|
|
seg.anim -= SEG_SPEED * dt;
|
|
if (seg.anim < target) 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 (auto& seg : info_segments_) {
|
|
if (seg.anim > 0.0F && !seg.text.empty()) {
|
|
int w = seg.mono_digits
|
|
? font_->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
|
|
: font_->width(seg.text.c_str());
|
|
total_w += w * Easing::outQuad(seg.anim);
|
|
}
|
|
}
|
|
if (total_w > 0.0F) {
|
|
float eased_y = Easing::outQuad(info_anim_);
|
|
int ch = font_->charHeight();
|
|
int final_y;
|
|
int start_y;
|
|
if (info_visible_pos_ == Options::RenderInfoPosition::TOP) {
|
|
final_y = 1;
|
|
start_y = -ch - 1;
|
|
} else {
|
|
final_y = SCREEN_H - ch - 1;
|
|
start_y = SCREEN_H;
|
|
}
|
|
int info_y = start_y + static_cast<int>((final_y - start_y) * eased_y);
|
|
|
|
// Dibuixa cada segment en la seva posició x acumulada
|
|
float cur_x = (SCREEN_W - total_w) / 2.0F;
|
|
for (auto& seg : info_segments_) {
|
|
if (seg.anim > 0.01F && !seg.text.empty()) {
|
|
int xi = static_cast<int>(cur_x);
|
|
int seg_w = seg.mono_digits
|
|
? font_->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
|
|
: font_->width(seg.text.c_str());
|
|
if (seg.mono_digits) {
|
|
font_->drawMonoDigits(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color, DIGIT_CELL);
|
|
font_->drawMonoDigits(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color, DIGIT_CELL);
|
|
} else {
|
|
font_->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color);
|
|
font_->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color);
|
|
}
|
|
cur_x += seg_w * Easing::outQuad(seg.anim);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Elimina les acabades
|
|
notifications_.erase(
|
|
std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }),
|
|
notifications_.end());
|
|
|
|
// 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() && 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);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
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<std::string>& lines,
|
|
float duration_seconds,
|
|
NotifPosition pos,
|
|
NotifStyle style,
|
|
Uint32 accent_color,
|
|
Uint32 text_color) {
|
|
// Reemplaça la notificació anterior
|
|
notifications_.clear();
|
|
|
|
Notification notif;
|
|
notif.lines = lines;
|
|
notif.pos = pos;
|
|
notif.style = style;
|
|
notif.accent_color = accent_color;
|
|
notif.text_color = text_color;
|
|
notif.duration = duration_seconds;
|
|
|
|
// 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<int>(lines.size());
|
|
notif.box_h = line_count * line_h + (line_count - 1) * 1 + NOTIF_PADDING_V * 2;
|
|
|
|
notifications_.push_back(notif);
|
|
}
|
|
|
|
void toggleRenderInfo() { cycleRenderInfo(+1); }
|
|
|
|
void cycleRenderInfo(int dir) {
|
|
// Seqüència: OFF → TOP → BOTTOM → OFF
|
|
int pos = static_cast<int>(Options::render_info.position);
|
|
pos = (pos + (dir >= 0 ? 1 : -1) + 3) % 3;
|
|
Options::render_info.position = static_cast<Options::RenderInfoPosition>(pos);
|
|
}
|
|
|
|
void setRenderInfoSegments(const char* s0, const char* s1, const char* s2, const char* s3, unsigned int mono_mask) {
|
|
const char* segs[INFO_SEGMENT_COUNT] = {s0, s1, s2, s3};
|
|
for (int i = 0; i < INFO_SEGMENT_COUNT; i++) {
|
|
info_segments_[i].mono_digits = (mono_mask >> i) & 1u;
|
|
if (segs[i] != nullptr && *segs[i] != '\0') {
|
|
info_segments_[i].text = segs[i];
|
|
info_segments_[i].visible = true;
|
|
} else {
|
|
info_segments_[i].visible = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
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_;
|
|
}
|
|
|
|
auto handleEscape() -> bool {
|
|
if (!esc_waiting_) {
|
|
// Primera pulsació: mostra avís i consumeix
|
|
esc_waiting_ = true;
|
|
showNotification(Locale::get("notifications.exit_double_esc"), 2.0F);
|
|
return true; // Consumit
|
|
}
|
|
// Segona pulsació: deixa passar
|
|
esc_waiting_ = false;
|
|
return false;
|
|
}
|
|
|
|
} // namespace Overlay
|