Files
aee/source/core/rendering/overlay.cpp
T

449 lines
17 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 : std::uint8_t { 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 : std::uint8_t { 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;
}
}
}
static void updateNotifFsm(Notification& notif, float dt) {
switch (notif.status) {
case Status::RISING:
notif.anim += SLIDE_SPEED * dt;
if (notif.anim >= 1.0F) {
notif.anim = 1.0F;
notif.status = Status::STAY;
notif.timer = 0.0F;
}
break;
case Status::STAY:
notif.timer += dt;
if (notif.timer >= notif.duration) {
notif.status = Status::VANISHING;
}
break;
case Status::VANISHING:
notif.anim -= SLIDE_SPEED * dt;
if (notif.anim <= 0.0F) {
notif.status = Status::FINISHED;
}
break;
case Status::FINISHED:
break;
}
}
static void computeNotifBoxPos(const Notification& notif, int& box_x, int& box_y) {
switch (notif.pos) {
case NotifPosition::TOP_LEFT_SLIDE:
box_x = NOTIF_MARGIN_X - static_cast<int>((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X));
box_y = NOTIF_MARGIN_Y;
break;
case NotifPosition::TOP_CENTER_DROP:
box_x = (SCREEN_W - notif.box_w) / 2;
box_y = NOTIF_MARGIN_Y - static_cast<int>((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y));
break;
}
}
static void drawNotifTextLine(Uint32* pixel_data, const std::string& line, int line_x, int line_y, const Notification& notif) {
if (notif.style == NotifStyle::SHADOW) {
font->draw(pixel_data, line_x + 1, line_y + 1, line.c_str(), notif.accent_color);
} else if (notif.style == NotifStyle::OUTLINE) {
font->draw(pixel_data, line_x, line_y - 1, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x, line_y + 1, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x - 1, line_y, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x + 1, line_y, line.c_str(), notif.accent_color);
}
font->draw(pixel_data, line_x, line_y, line.c_str(), notif.text_color);
}
static void renderOneNotification(Uint32* pixel_data, const Notification& notif) {
int box_x = 0;
int box_y = 0;
computeNotifBoxPos(notif, box_x, box_y);
if (notif.style == NotifStyle::BOX) {
drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color);
}
const int LINE_H = font->charHeight();
int line_y = box_y + NOTIF_PADDING_V;
for (const auto& line : notif.lines) {
const int LINE_W = font->width(line.c_str());
const int LINE_X = box_x + ((notif.box_w - LINE_W) / 2);
drawNotifTextLine(pixel_data, line, LINE_X, line_y, notif);
line_y += LINE_H + 1;
}
}
static void updateRenderInfoFsm(float dt) {
const auto DESIRED = Options::render_info.position;
if (DESIRED == info_visible_pos) {
if (info_anim < 1.0F) {
info_anim = std::min(info_anim + (INFO_SLIDE_SPEED * dt), 1.0F);
}
} else if (info_visible_pos == Options::RenderInfoPosition::OFF) {
info_visible_pos = DESIRED;
info_anim = 0.0F;
} else {
info_anim -= INFO_SLIDE_SPEED * dt;
if (info_anim <= 0.0F) {
info_anim = 0.0F;
info_visible_pos = DESIRED;
}
}
for (auto& seg : info_segments) {
const float TARGET = seg.visible ? 1.0F : 0.0F;
if (seg.anim < TARGET) {
seg.anim = std::min(seg.anim + (SEG_SPEED * dt), TARGET);
} else if (seg.anim > TARGET) {
seg.anim = std::max(seg.anim - (SEG_SPEED * dt), TARGET);
}
}
}
static auto computeInfoTotalWidth(int digit_cell) -> float {
float total_w = 0.0F;
for (const auto& seg : info_segments) {
if (seg.anim > 0.0F && !seg.text.empty()) {
const int W = seg.mono_digits
? font->widthMonoDigits(seg.text.c_str(), digit_cell)
: font->width(seg.text.c_str());
total_w += static_cast<float>(W) * Easing::outQuad(seg.anim);
}
}
return total_w;
}
static void drawInfoSegment(Uint32* pixel_data, const InfoSegment& seg, int xi, int info_y, int digit_cell) {
if (seg.mono_digits) {
font->drawMonoDigits(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color, digit_cell);
font->drawMonoDigits(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color, digit_cell);
} else {
font->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color);
font->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color);
}
}
static void renderRenderInfo(Uint32* pixel_data) {
if (info_visible_pos == Options::RenderInfoPosition::OFF || info_anim <= 0.0F) {
return;
}
const int DIGIT_CELL = font->charBoxWidth() - 1;
const float TOTAL_W = computeInfoTotalWidth(DIGIT_CELL);
if (TOTAL_W <= 0.0F) {
return;
}
const float EASED_Y = Easing::outQuad(info_anim);
const int CH = font->charHeight();
const int FINAL_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? 1 : SCREEN_H - CH - 1;
const int START_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? -CH - 1 : SCREEN_H;
const int INFO_Y = START_Y + static_cast<int>(static_cast<float>(FINAL_Y - START_Y) * EASED_Y);
float cur_x = (SCREEN_W - TOTAL_W) / 2.0F;
for (const auto& seg : info_segments) {
if (seg.anim <= 0.01F || seg.text.empty()) {
continue;
}
const int XI = static_cast<int>(cur_x);
const int SEG_W = seg.mono_digits
? font->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
: font->width(seg.text.c_str());
drawInfoSegment(pixel_data, seg, XI, INFO_Y, DIGIT_CELL);
cur_x += static_cast<float>(SEG_W) * Easing::outQuad(seg.anim);
}
}
static void renderPauseIndicator(Uint32* pixel_data) {
if ((Director::get() == nullptr) || !Director::get()->isPaused()) {
return;
}
const char* pause_text = Locale::get("notifications.pause");
const int W = font->width(pause_text);
const int X = SCREEN_W - W - 4;
const int Y = 4;
font->draw(pixel_data, X, Y - 1, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X, Y + 1, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X - 1, Y, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X + 1, Y, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X, Y, pause_text, 0xFF0000FF);
}
static void emitCreditsLines(const char* role_key, const char* name_key) {
showNotification(
{std::string(Locale::get(role_key)), std::string(Locale::get(name_key))},
CREDITS_HOLD,
NotifPosition::TOP_CENTER_DROP,
NotifStyle::OUTLINE,
CREDITS_BG,
CREDITS_FG);
}
static void advanceCredits(float dt) {
if (credits_phase == CreditsPhase::IDLE) {
return;
}
credits_timer += dt;
switch (credits_phase) {
case CreditsPhase::DELAY:
if (credits_timer >= CREDITS_DELAY) {
emitCreditsLines("credits.port_role", "credits.port_name");
credits_phase = CreditsPhase::PLAYING_1;
credits_timer = 0.0F;
}
break;
case CreditsPhase::PLAYING_1:
if (notifications.empty()) {
credits_phase = CreditsPhase::GAP;
credits_timer = 0.0F;
}
break;
case CreditsPhase::GAP:
if (credits_timer >= CREDITS_GAP) {
emitCreditsLines("credits.modern_role", "credits.modern_name");
credits_phase = CreditsPhase::PLAYING_2;
credits_timer = 0.0F;
}
break;
case CreditsPhase::PLAYING_2:
if (notifications.empty()) {
credits_phase = CreditsPhase::IDLE;
credits_timer = 0.0F;
}
break;
case CreditsPhase::IDLE:
break;
}
}
void render(Uint32* pixel_data) {
if (!font || (pixel_data == nullptr)) {
return;
}
const Uint32 NOW = SDL_GetTicks();
const float DT = static_cast<float>(NOW - last_ticks) / 1000.0F;
last_ticks = NOW;
for (auto& notif : notifications) {
updateNotifFsm(notif, DT);
if (notif.status != Status::FINISHED) {
renderOneNotification(pixel_data, notif);
}
}
updateRenderInfoFsm(DT);
renderRenderInfo(pixel_data);
std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; });
if (esc_waiting && notifications.empty()) {
esc_waiting = false;
}
renderPauseIndicator(pixel_data);
advanceCredits(DT);
std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; });
if (Menu::isVisible()) {
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());
max_w = std::max(w, max_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) != 0U);
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