treballant en el overlay, el text i les notificacions
This commit is contained in:
119
source/core/rendering/overlay.cpp
Normal file
119
source/core/rendering/overlay.cpp
Normal file
@@ -0,0 +1,119 @@
|
||||
#include "core/rendering/overlay.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/rendering/text.hpp"
|
||||
|
||||
namespace Overlay {
|
||||
|
||||
static std::unique_ptr<Text> font_;
|
||||
|
||||
// --- Notificacions amb animació ---
|
||||
|
||||
enum class Status { RISING, STAY, VANISHING, FINISHED };
|
||||
|
||||
struct Notification {
|
||||
std::string message;
|
||||
Status status{Status::RISING};
|
||||
float y_offset{0.0F}; // 0 = fora de pantalla, 1 = posició final
|
||||
float timer{0.0F};
|
||||
float duration{2.0F};
|
||||
};
|
||||
|
||||
static std::vector<Notification> notifications_;
|
||||
static Uint32 last_ticks_ = 0;
|
||||
|
||||
static constexpr float SLIDE_SPEED = 4.0F; // velocitat d'animació (unitats/segon)
|
||||
static constexpr int BAR_HEIGHT = 12; // alçada de la barra
|
||||
static constexpr int TEXT_Y_OFFSET = 2; // offset del text dins la barra
|
||||
static constexpr Uint32 BG_COLOR = 0xFF1A1A2E; // fons blau fosc
|
||||
static constexpr Uint32 TEXT_COLOR = 0xFF00FFFF; // cyan
|
||||
static constexpr int SCREEN_W = 320;
|
||||
static constexpr int SCREEN_H = 200;
|
||||
|
||||
void init() {
|
||||
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
|
||||
last_ticks_ = SDL_GetTicks();
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
font_.reset();
|
||||
notifications_.clear();
|
||||
}
|
||||
|
||||
static void drawBar(Uint32* pixel_data, int y, int h, Uint32 color) {
|
||||
for (int row = y; row < y + h; row++) {
|
||||
if (row < 0 || row >= SCREEN_H) continue;
|
||||
for (int col = 0; col < SCREEN_W; col++) {
|
||||
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.y_offset += SLIDE_SPEED * dt;
|
||||
if (notif.y_offset >= 1.0F) {
|
||||
notif.y_offset = 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.y_offset -= SLIDE_SPEED * dt;
|
||||
if (notif.y_offset <= 0.0F) {
|
||||
notif.status = Status::FINISHED;
|
||||
}
|
||||
break;
|
||||
|
||||
case Status::FINISHED:
|
||||
break;
|
||||
}
|
||||
|
||||
if (notif.status == Status::FINISHED) continue;
|
||||
|
||||
// Posició: puja des de sota la pantalla
|
||||
int bar_y = SCREEN_H - static_cast<int>(notif.y_offset * BAR_HEIGHT);
|
||||
|
||||
// Pinta fons
|
||||
drawBar(pixel_data, bar_y, BAR_HEIGHT, BG_COLOR);
|
||||
|
||||
// Pinta text centrat
|
||||
font_->drawCentered(pixel_data, bar_y + TEXT_Y_OFFSET, notif.message.c_str(), TEXT_COLOR);
|
||||
}
|
||||
|
||||
// Elimina les acabades
|
||||
notifications_.erase(
|
||||
std::remove_if(notifications_.begin(), notifications_.end(),
|
||||
[](const Notification& n) { return n.status == Status::FINISHED; }),
|
||||
notifications_.end());
|
||||
}
|
||||
|
||||
void showNotification(const char* text, float duration_seconds) {
|
||||
// Si ja hi ha una notificació, la reemplaça (no apilem)
|
||||
notifications_.clear();
|
||||
notifications_.push_back({text, Status::RISING, 0.0F, 0.0F, duration_seconds});
|
||||
}
|
||||
|
||||
} // namespace Overlay
|
||||
14
source/core/rendering/overlay.hpp
Normal file
14
source/core/rendering/overlay.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace Overlay {
|
||||
void init();
|
||||
void destroy();
|
||||
|
||||
// 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
|
||||
void showNotification(const char* text, float duration_seconds = 2.0F);
|
||||
} // namespace Overlay
|
||||
108
source/core/rendering/screen.cpp
Normal file
108
source/core/rendering/screen.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
#include "core/rendering/screen.hpp"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "core/rendering/overlay.hpp"
|
||||
#include "game/defines.hpp"
|
||||
#include "game/options.hpp"
|
||||
|
||||
Screen* Screen::instance_ = nullptr;
|
||||
|
||||
void Screen::init() {
|
||||
instance_ = new Screen();
|
||||
}
|
||||
|
||||
void Screen::destroy() {
|
||||
delete instance_;
|
||||
instance_ = nullptr;
|
||||
}
|
||||
|
||||
auto Screen::get() -> Screen* {
|
||||
return instance_;
|
||||
}
|
||||
|
||||
Screen::Screen() {
|
||||
// Carrega opcions guardades
|
||||
zoom_ = Options::window.zoom;
|
||||
fullscreen_ = Options::window.fullscreen;
|
||||
|
||||
calculateMaxZoom();
|
||||
|
||||
if (zoom_ < 1) zoom_ = 1;
|
||||
if (zoom_ > max_zoom_) zoom_ = max_zoom_;
|
||||
|
||||
int w = GAME_WIDTH * zoom_;
|
||||
int h = GAME_HEIGHT * zoom_;
|
||||
|
||||
window_ = SDL_CreateWindow(Texts::WINDOW_TITLE, w, h, fullscreen_ ? SDL_WINDOW_FULLSCREEN : 0);
|
||||
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
||||
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH, GAME_HEIGHT, SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
|
||||
texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, GAME_WIDTH, GAME_HEIGHT);
|
||||
SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST);
|
||||
|
||||
std::cout << "Screen initialized: " << w << "x" << h << " (zoom " << zoom_ << ", max " << max_zoom_ << ")\n";
|
||||
}
|
||||
|
||||
Screen::~Screen() {
|
||||
// Guarda opcions abans de destruir
|
||||
Options::window.zoom = zoom_;
|
||||
Options::window.fullscreen = fullscreen_;
|
||||
|
||||
if (texture_) SDL_DestroyTexture(texture_);
|
||||
if (renderer_) SDL_DestroyRenderer(renderer_);
|
||||
if (window_) SDL_DestroyWindow(window_);
|
||||
}
|
||||
|
||||
void Screen::present(Uint32* pixel_data) {
|
||||
Overlay::render(pixel_data);
|
||||
SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32));
|
||||
SDL_RenderClear(renderer_);
|
||||
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
|
||||
SDL_RenderPresent(renderer_);
|
||||
}
|
||||
|
||||
void Screen::toggleFullscreen() {
|
||||
fullscreen_ = !fullscreen_;
|
||||
SDL_SetWindowFullscreen(window_, fullscreen_);
|
||||
if (!fullscreen_) {
|
||||
adjustWindowSize();
|
||||
}
|
||||
std::cout << (fullscreen_ ? "Fullscreen ON\n" : "Fullscreen OFF\n");
|
||||
}
|
||||
|
||||
void Screen::incZoom() {
|
||||
if (fullscreen_ || zoom_ >= max_zoom_) return;
|
||||
zoom_++;
|
||||
adjustWindowSize();
|
||||
}
|
||||
|
||||
void Screen::decZoom() {
|
||||
if (fullscreen_ || zoom_ <= 1) return;
|
||||
zoom_--;
|
||||
adjustWindowSize();
|
||||
}
|
||||
|
||||
void Screen::setZoom(int zoom) {
|
||||
if (zoom < 1 || zoom > max_zoom_ || fullscreen_) return;
|
||||
zoom_ = zoom;
|
||||
adjustWindowSize();
|
||||
}
|
||||
|
||||
void Screen::adjustWindowSize() {
|
||||
int w = GAME_WIDTH * zoom_;
|
||||
int h = GAME_HEIGHT * zoom_;
|
||||
SDL_SetWindowSize(window_, w, h);
|
||||
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||
}
|
||||
|
||||
void Screen::calculateMaxZoom() {
|
||||
SDL_DisplayID display = SDL_GetPrimaryDisplay();
|
||||
const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display);
|
||||
if (mode) {
|
||||
int max_w = mode->w / GAME_WIDTH;
|
||||
int max_h = mode->h / GAME_HEIGHT;
|
||||
max_zoom_ = (max_w < max_h) ? max_w : max_h;
|
||||
if (max_zoom_ < 1) max_zoom_ = 1;
|
||||
}
|
||||
}
|
||||
47
source/core/rendering/screen.hpp
Normal file
47
source/core/rendering/screen.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class Screen {
|
||||
public:
|
||||
static void init();
|
||||
static void destroy();
|
||||
static auto get() -> Screen*;
|
||||
|
||||
// Presentació — rep el buffer ARGB de 320x200 de JD8
|
||||
void present(Uint32* pixel_data);
|
||||
|
||||
// Gestió de finestra
|
||||
void toggleFullscreen();
|
||||
void incZoom();
|
||||
void decZoom();
|
||||
void setZoom(int zoom);
|
||||
|
||||
// Getters
|
||||
[[nodiscard]] auto isFullscreen() const -> bool { return fullscreen_; }
|
||||
[[nodiscard]] auto getZoom() const -> int { return zoom_; }
|
||||
[[nodiscard]] auto getWindow() -> SDL_Window* { return window_; }
|
||||
[[nodiscard]] auto getRenderer() -> SDL_Renderer* { return renderer_; }
|
||||
|
||||
private:
|
||||
Screen();
|
||||
~Screen();
|
||||
|
||||
void adjustWindowSize();
|
||||
void calculateMaxZoom();
|
||||
|
||||
static Screen* instance_;
|
||||
|
||||
SDL_Window* window_{nullptr};
|
||||
SDL_Renderer* renderer_{nullptr};
|
||||
SDL_Texture* texture_{nullptr}; // 320x200 streaming, ARGB8888
|
||||
|
||||
int zoom_{3};
|
||||
int max_zoom_{6};
|
||||
bool fullscreen_{false};
|
||||
|
||||
static constexpr int GAME_WIDTH = 320;
|
||||
static constexpr int GAME_HEIGHT = 200;
|
||||
};
|
||||
239
source/core/rendering/text.cpp
Normal file
239
source/core/rendering/text.cpp
Normal file
@@ -0,0 +1,239 @@
|
||||
#include "core/rendering/text.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "core/jail/jfile.hpp"
|
||||
|
||||
// Forward declarations de gif.h (inclòs des de jdraw8.cpp, no es pot incloure dos vegades)
|
||||
struct rgb;
|
||||
extern unsigned char* LoadGif(unsigned char* data, unsigned short* w, unsigned short* h);
|
||||
|
||||
Text::Text(const char* fnt_file, const char* gif_file) {
|
||||
loadBitmap(gif_file);
|
||||
loadFont(fnt_file);
|
||||
}
|
||||
|
||||
Text::~Text() {
|
||||
if (bitmap_) free(bitmap_);
|
||||
}
|
||||
|
||||
// --- UTF-8 ---
|
||||
|
||||
auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
|
||||
auto byte = static_cast<uint8_t>(*ptr);
|
||||
if (byte == 0) return 0;
|
||||
|
||||
uint32_t cp = 0;
|
||||
int extra = 0;
|
||||
|
||||
if (byte < 0x80) {
|
||||
cp = byte;
|
||||
} else if ((byte & 0xE0) == 0xC0) {
|
||||
cp = byte & 0x1F;
|
||||
extra = 1;
|
||||
} else if ((byte & 0xF0) == 0xE0) {
|
||||
cp = byte & 0x0F;
|
||||
extra = 2;
|
||||
} else if ((byte & 0xF8) == 0xF0) {
|
||||
cp = byte & 0x07;
|
||||
extra = 3;
|
||||
} else {
|
||||
ptr++;
|
||||
return 0xFFFD;
|
||||
}
|
||||
|
||||
ptr++;
|
||||
for (int i = 0; i < extra; i++) {
|
||||
auto cont = static_cast<uint8_t>(*ptr);
|
||||
if ((cont & 0xC0) != 0x80) return 0xFFFD;
|
||||
cp = (cp << 6) | (cont & 0x3F);
|
||||
ptr++;
|
||||
}
|
||||
|
||||
return cp;
|
||||
}
|
||||
|
||||
// --- Càrrega de font ---
|
||||
|
||||
void Text::loadFont(const char* fnt_file) {
|
||||
int filesize = 0;
|
||||
char* buffer = file_getfilebuffer(fnt_file, filesize, true);
|
||||
if (!buffer) {
|
||||
std::cerr << "Text: unable to load font file: " << fnt_file << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
std::istringstream stream(std::string(buffer, filesize));
|
||||
free(buffer);
|
||||
|
||||
std::string line;
|
||||
int glyph_index = 0;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
// Ignora comentaris i línies buides
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
// Elimina comentaris inline
|
||||
auto comment_pos = line.find('#');
|
||||
if (comment_pos != std::string::npos) {
|
||||
line = line.substr(0, comment_pos);
|
||||
}
|
||||
|
||||
// Parseja directives
|
||||
if (line.find("box_width") == 0) {
|
||||
sscanf(line.c_str(), "box_width %d", &box_width_);
|
||||
continue;
|
||||
}
|
||||
if (line.find("box_height") == 0) {
|
||||
sscanf(line.c_str(), "box_height %d", &box_height_);
|
||||
continue;
|
||||
}
|
||||
if (line.find("columns") == 0) {
|
||||
sscanf(line.c_str(), "columns %d", &columns_);
|
||||
continue;
|
||||
}
|
||||
if (line.find("cell_spacing") == 0) {
|
||||
sscanf(line.c_str(), "cell_spacing %d", &cell_spacing_);
|
||||
continue;
|
||||
}
|
||||
if (line.find("row_spacing") == 0) {
|
||||
sscanf(line.c_str(), "row_spacing %d", &row_spacing_);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Línies de glifos: "codepoint width"
|
||||
uint32_t codepoint = 0;
|
||||
int visual_width = 0;
|
||||
if (sscanf(line.c_str(), "%u %d", &codepoint, &visual_width) == 2) {
|
||||
int col = glyph_index % columns_;
|
||||
int row = glyph_index / columns_;
|
||||
|
||||
GlyphInfo glyph{};
|
||||
glyph.x = col * (box_width_ + cell_spacing_) + cell_spacing_;
|
||||
glyph.y = row * (box_height_ + (row_spacing_ > 0 ? row_spacing_ : cell_spacing_)) + cell_spacing_;
|
||||
glyph.w = visual_width;
|
||||
|
||||
glyphs_[codepoint] = glyph;
|
||||
glyph_index++;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Text: loaded font with " << glyphs_.size() << " glyphs (" << box_width_ << "x" << box_height_ << ")\n";
|
||||
}
|
||||
|
||||
void Text::loadBitmap(const char* gif_file) {
|
||||
int filesize = 0;
|
||||
char* buffer = file_getfilebuffer(gif_file, filesize);
|
||||
if (!buffer) {
|
||||
std::cerr << "Text: unable to load bitmap: " << gif_file << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// Extrau dimensions del header GIF (bytes 6-7 = width, 8-9 = height, little-endian)
|
||||
auto* raw = reinterpret_cast<unsigned char*>(buffer);
|
||||
int w = raw[6] | (raw[7] << 8);
|
||||
int h = raw[8] | (raw[9] << 8);
|
||||
|
||||
unsigned short gw = 0, gh = 0;
|
||||
Uint8* pixels = LoadGif(raw, &gw, &gh);
|
||||
if (!pixels) {
|
||||
std::cerr << "Text: unable to decode GIF: " << gif_file << '\n';
|
||||
free(buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
bitmap_width_ = w;
|
||||
bitmap_height_ = h;
|
||||
bitmap_ = pixels;
|
||||
|
||||
free(buffer);
|
||||
std::cout << "Text: bitmap loaded " << w << "x" << h << '\n';
|
||||
}
|
||||
|
||||
// --- Renderitzat ---
|
||||
|
||||
void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const {
|
||||
if (!bitmap_ || !pixel_data) return;
|
||||
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
|
||||
while (*ptr) {
|
||||
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;
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
void Text::drawCentered(Uint32* pixel_data, int y, const char* text, Uint32 color) const {
|
||||
int w = width(text);
|
||||
int x = (SCREEN_WIDTH - w) / 2;
|
||||
draw(pixel_data, x, y, text, color);
|
||||
}
|
||||
|
||||
auto Text::width(const char* text) const -> int {
|
||||
const char* ptr = text;
|
||||
int w = 0;
|
||||
bool first = true;
|
||||
|
||||
while (*ptr) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
}
|
||||
|
||||
if (!first) w += 1; // kerning
|
||||
first = false;
|
||||
|
||||
if (it != glyphs_.end()) {
|
||||
w += it->second.w;
|
||||
} else {
|
||||
w += box_width_;
|
||||
}
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
47
source/core/rendering/text.hpp
Normal file
47
source/core/rendering/text.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
class Text {
|
||||
public:
|
||||
Text(const char* fnt_file, const char* gif_file);
|
||||
~Text();
|
||||
|
||||
// Pinta texto sobre un buffer ARGB de 320x200
|
||||
void draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const;
|
||||
void drawCentered(Uint32* pixel_data, int y, const char* text, Uint32 color) const;
|
||||
|
||||
// Calcula ancho en píxeles d'un text
|
||||
[[nodiscard]] auto width(const char* text) const -> int;
|
||||
[[nodiscard]] auto charHeight() const -> int { return box_height_; }
|
||||
|
||||
private:
|
||||
struct GlyphInfo {
|
||||
int x, y; // posició en el bitmap
|
||||
int w; // ample visual
|
||||
};
|
||||
|
||||
int box_width_{0};
|
||||
int box_height_{0};
|
||||
int columns_{0};
|
||||
int cell_spacing_{0};
|
||||
int row_spacing_{0};
|
||||
|
||||
Uint8* bitmap_{nullptr}; // píxels 8-bit del GIF de la font
|
||||
int bitmap_width_{0};
|
||||
int bitmap_height_{0};
|
||||
|
||||
std::unordered_map<uint32_t, GlyphInfo> glyphs_;
|
||||
|
||||
static auto nextCodepoint(const char*& ptr) -> uint32_t;
|
||||
|
||||
void loadFont(const char* fnt_file);
|
||||
void loadBitmap(const char* gif_file);
|
||||
|
||||
static constexpr int SCREEN_WIDTH = 320;
|
||||
static constexpr int SCREEN_HEIGHT = 200;
|
||||
};
|
||||
Reference in New Issue
Block a user