diff --git a/CMakeLists.txt b/CMakeLists.txt index b18c9a3..dddb314 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,8 +34,10 @@ set(APP_SOURCES source/main.cpp source/core/system/director.cpp source/core/rendering/screen.cpp + source/core/rendering/render_info.cpp source/core/rendering/gif.cpp source/core/rendering/surface.cpp + source/core/rendering/text.cpp source/core/rendering/sprite/sprite.cpp source/core/rendering/sprite/moving_sprite.cpp source/core/rendering/sprite/animated_sprite.cpp diff --git a/data/font/8bithud.fnt b/data/font/8bithud.fnt new file mode 100644 index 0000000..5cdf2e8 --- /dev/null +++ b/data/font/8bithud.fnt @@ -0,0 +1,132 @@ +# Font: 8bithud — generado desde 8-bit-hud.ttf size 5 +# Generado con tools/font_gen/font_gen.py + +box_width 8 +box_height 8 +columns 15 + +# codepoint_decimal ancho_visual +32 3 # U+0020 +33 2 # ! +34 5 # " +35 6 # # +36 6 # $ +37 6 # % +38 6 # & +39 2 # ' +40 3 # ( +41 3 # ) +42 4 # * +43 3 # + +44 2 # , +45 3 # - +46 2 # . +47 4 # / +48 6 # 0 +49 3 # 1 +50 6 # 2 +51 6 # 3 +52 6 # 4 +53 6 # 5 +54 6 # 6 +55 6 # 7 +56 6 # 8 +57 6 # 9 +58 2 # : +59 2 # ; +60 4 # < +61 3 # = +62 4 # > +63 6 # ? +64 8 # @ +65 6 # A +66 6 # B +67 6 # C +68 6 # D +69 6 # E +70 6 # F +71 6 # G +72 6 # H +73 6 # I +74 6 # J +75 6 # K +76 6 # L +77 6 # M +78 6 # N +79 6 # O +80 6 # P +81 6 # Q +82 6 # R +83 6 # S +84 6 # T +85 6 # U +86 5 # V +87 6 # W +88 6 # X +89 6 # Y +90 6 # Z +91 3 # [ +92 4 # \ +93 3 # ] +94 4 # ^ +95 6 # _ +96 2 # ` +97 5 # a +98 5 # b +99 5 # c +100 5 # d +101 5 # e +102 5 # f +103 5 # g +104 5 # h +105 4 # i +106 5 # j +107 5 # k +108 5 # l +109 6 # m +110 5 # n +111 5 # o +112 5 # p +113 5 # q +114 5 # r +115 5 # s +116 4 # t +117 5 # u +118 5 # v +119 6 # w +120 4 # x +121 4 # y +122 5 # z +123 4 # { +124 1 # | +125 4 # } +126 4 # ~ +192 6 # À +193 6 # Á +200 6 # È +201 6 # É +204 6 # Ì +205 6 # Í +210 6 # Ò +211 6 # Ó +219 6 # Ù +218 6 # Ú +209 6 # Ñ +199 6 # Ç +224 5 # à +225 5 # á +232 5 # è +233 5 # é +236 4 # ì +237 4 # í +242 5 # ò +243 5 # ó +249 5 # ù +250 5 # ú +241 5 # ñ +231 5 # ç +161 2 # ¡ +191 6 # ¿ +171 4 # « +187 4 # » +183 2 # · diff --git a/data/font/8bithud.gif b/data/font/8bithud.gif new file mode 100644 index 0000000..9b08e47 Binary files /dev/null and b/data/font/8bithud.gif differ diff --git a/source/core/rendering/render_info.cpp b/source/core/rendering/render_info.cpp new file mode 100644 index 0000000..78c23bc --- /dev/null +++ b/source/core/rendering/render_info.cpp @@ -0,0 +1,114 @@ +#include "core/rendering/render_info.hpp" + +#include +#include +#include + +#include "core/rendering/screen.hpp" +#include "core/rendering/text.hpp" +#include "core/resources/resource_cache.hpp" +#include "game/options.hpp" +#include "utils/defines.hpp" + +namespace { + + // La paleta de 8bithud.gif només té 2 entrades (0=negre, 1=blanc); per a + // poder colorar el text lliurement caldrà una paleta compartida, com al + // projecte_2026. De moment ens quedem amb blanc. + constexpr Uint8 TEXT_COLOR = 1; + constexpr int TEXT_SIZE = 8; + constexpr int PADDING_V = (RenderInfo::HEIGHT - TEXT_SIZE) / 2; + + auto prettify(std::string s) -> std::string { + std::ranges::replace(s, '-', ' '); + std::ranges::transform(s, s.begin(), [](unsigned char c) { return std::tolower(c); }); + return s; + } + +} // namespace + +auto RenderInfo::get() -> RenderInfo& { + static RenderInfo instance_; + return instance_; +} + +RenderInfo::~RenderInfo() = default; + +void RenderInfo::init() { + // El fitxer s'indexa al Cache perquè també el puguen reutilitzar altres + // HUDs que vulguen la mateixa font sense recarregar-la. + Resource::Cache::get().loadSurface("8bithud.gif", "data/font/8bithud.gif"); + auto font_surface = Resource::Cache::get().getSurface("8bithud.gif"); + text_ = std::make_unique(font_surface, "data/font/8bithud.fnt"); + + // Restaura l'estat guardat a config.yaml (video.render_info). + if (Options::video.render_info) { + status_ = Status::ACTIVE; + y_ = 0.0F; + } +} + +void RenderInfo::update(int delta_ms) { + const float DT = static_cast(delta_ms) / 1000.0F; + switch (status_) { + case Status::RISING: + y_ += SLIDE_SPEED * DT; + if (y_ >= 0.0F) { + y_ = 0.0F; + status_ = Status::ACTIVE; + } + break; + case Status::VANISHING: + y_ -= SLIDE_SPEED * DT; + if (y_ <= static_cast(-HEIGHT)) { + y_ = static_cast(-HEIGHT); + status_ = Status::HIDDEN; + } + break; + default: + break; + } +} + +void RenderInfo::render() { + if (status_ == Status::HIDDEN || text_ == nullptr) { return; } + + std::string line = std::to_string(Screen::get().getLastFPS()) + " fps"; + + const auto& driver = Screen::get().getGPUDriver(); + line += " | " + (driver.empty() ? std::string("sdl") : driver); + + if (Options::video.shader.enabled) { + const std::string& shader = Options::video.shader.current_shader; + const std::string& preset = (shader == "crtpi") + ? Options::video.shader.current_crtpi_preset + : Options::video.shader.current_postfx_preset; + line += " | " + shader + " " + prettify(preset); + } + + line = prettify(line); + + text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, + GameCanvas::CENTER_X, + static_cast(y_) + PADDING_V, + line, + 1, + TEXT_COLOR); +} + +void RenderInfo::toggle() { + switch (status_) { + case Status::HIDDEN: + status_ = Status::RISING; + Options::video.render_info = true; + Options::saveToFile(); + break; + case Status::ACTIVE: + status_ = Status::VANISHING; + Options::video.render_info = false; + Options::saveToFile(); + break; + default: + break; + } +} diff --git a/source/core/rendering/render_info.hpp b/source/core/rendering/render_info.hpp new file mode 100644 index 0000000..f89c0ca --- /dev/null +++ b/source/core/rendering/render_info.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +class Text; + +// Overlay de debug que mostra FPS, driver GPU i shader + preset actiu. Es +// dibuixa a la part superior de la pantalla amb una animació de lliscament +// (RISING / ACTIVE / VANISHING / HIDDEN). L'activa la tecla F3 a l'escena. +class RenderInfo { + public: + static auto get() -> RenderInfo&; + + RenderInfo(const RenderInfo&) = delete; + RenderInfo(RenderInfo&&) = delete; + auto operator=(const RenderInfo&) -> RenderInfo& = delete; + auto operator=(RenderInfo&&) -> RenderInfo& = delete; + + // Carrega la font 8bithud. S'ha de cridar després de configurar Screen. + void init(); + + void update(int delta_ms); + void render(); + void toggle(); + + [[nodiscard]] auto isActive() const -> bool { return status_ != Status::HIDDEN; } + + // Alçada fixa de l'overlay: TEXT_SIZE (8) + padding vertical. + static constexpr int HEIGHT = 10; + static constexpr float SLIDE_SPEED = 120.0F; + + private: + enum class Status : std::uint8_t { HIDDEN, + RISING, + ACTIVE, + VANISHING }; + + RenderInfo() = default; + ~RenderInfo(); + + std::unique_ptr text_; + Status status_{Status::HIDDEN}; + float y_{static_cast(-HEIGHT)}; +}; diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index 1b5d065..632f5e1 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -110,12 +110,14 @@ void Screen::createSdl() { backend->setPreferredDriver(config_.preferred_driver); backend->setVSync(config_.vsync); if (backend->init(window_, canvas_width_, canvas_height_)) { - std::cout << "Screen: path GPU actiu (driver: " << backend->getDriverName() << ")\n"; + gpu_driver_ = backend->getDriverName(); + std::cout << "Screen: path GPU actiu (driver: " << gpu_driver_ << ")\n"; shader_backend_ = std::move(backend); return; } std::cerr << "Screen: GPU no disponible, fallback a SDL_Renderer\n"; } + gpu_driver_ = "sdl"; renderer_ = SDL_CreateRenderer(window_, nullptr); if (renderer_ == nullptr) { @@ -160,6 +162,9 @@ void Screen::clear(std::uint8_t r, std::uint8_t g, std::uint8_t b) { } void Screen::present() { + fps_.increment(); + fps_.calculate(SDL_GetTicks()); + if (shader_backend_ != nullptr) { shader_backend_->uploadPixels(canvas_.data(), canvas_width_, canvas_height_); shader_backend_->render(); diff --git a/source/core/rendering/screen.hpp b/source/core/rendering/screen.hpp index 637b455..30e74ab 100644 --- a/source/core/rendering/screen.hpp +++ b/source/core/rendering/screen.hpp @@ -62,6 +62,10 @@ class Screen { [[nodiscard]] auto getCanvasWidth() const -> int { return canvas_width_; } [[nodiscard]] auto getCanvasHeight() const -> int { return canvas_height_; } + // Mètriques per a overlays de debug (RenderInfo). + [[nodiscard]] auto getLastFPS() const -> int { return fps_.last_value; } + [[nodiscard]] auto getGPUDriver() const -> const std::string& { return gpu_driver_; } + private: Screen() = default; ~Screen() = default; @@ -69,6 +73,23 @@ class Screen { void destroySdl(); void createSdl(); + // Comptador intern de frames per segon. S'incrementa a cada present() i + // es recalcula quan ha passat un segon complet (SDL_GetTicks en ms). + struct FPS { + std::uint32_t ticks{0}; + int frame_count{0}; + int last_value{0}; + + void increment() { ++frame_count; } + void calculate(std::uint32_t current_ticks) { + if (current_ticks - ticks >= 1000) { + last_value = frame_count; + frame_count = 0; + ticks = current_ticks; + } + } + }; + bool subsystem_initialized_{false}; Config config_{}; @@ -83,4 +104,7 @@ class Screen { int canvas_width_{0}; int canvas_height_{0}; std::vector canvas_; + + FPS fps_{}; + std::string gpu_driver_; }; diff --git a/source/core/rendering/sprite/sprite.cpp b/source/core/rendering/sprite/sprite.cpp index cfa659f..02579c9 100644 --- a/source/core/rendering/sprite/sprite.cpp +++ b/source/core/rendering/sprite/sprite.cpp @@ -25,6 +25,10 @@ void Sprite::render() { surface_->render(static_cast(pos_.x), static_cast(pos_.y), &clip_); } +void Sprite::render(Uint8 source_color, Uint8 target_color) { + surface_->renderWithColorReplace(static_cast(pos_.x), static_cast(pos_.y), source_color, target_color, &clip_); +} + void Sprite::setPosition(float x, float y) { pos_.x = x; pos_.y = y; diff --git a/source/core/rendering/sprite/sprite.hpp b/source/core/rendering/sprite/sprite.hpp index 7ffab5c..c73d043 100644 --- a/source/core/rendering/sprite/sprite.hpp +++ b/source/core/rendering/sprite/sprite.hpp @@ -18,6 +18,9 @@ class Sprite { virtual void update(float delta_time); virtual void render(); + // Renderitza substituint l'índex source_color per target_color (ús típic: + // la classe Text, que recolora glifs monocroms per paràmetre). + virtual void render(Uint8 source_color, Uint8 target_color); virtual void clear(); [[nodiscard]] auto getX() const -> float { return pos_.x; } diff --git a/source/core/rendering/surface.cpp b/source/core/rendering/surface.cpp index 32cf7eb..b55ebf4 100644 --- a/source/core/rendering/surface.cpp +++ b/source/core/rendering/surface.cpp @@ -113,3 +113,33 @@ void Surface::render(int x, int y, const SDL_FRect* src_rect, SDL_FlipMode flip) } } } + +void Surface::renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 target_color, const SDL_FRect* src_rect, SDL_FlipMode flip) const { + Uint32* dst = Screen::get().getCanvasArgb(); + const int DST_W = Screen::get().getCanvasWidth(); + const int DST_H = Screen::get().getCanvasHeight(); + + const int SX = (src_rect != nullptr) ? static_cast(src_rect->x) : 0; + const int SY = (src_rect != nullptr) ? static_cast(src_rect->y) : 0; + int sw = (src_rect != nullptr) ? static_cast(src_rect->w) : surface_data_->width; + int sh = (src_rect != nullptr) ? static_cast(src_rect->h) : surface_data_->height; + + sw = std::min(sw, surface_data_->width - SX); + sh = std::min(sh, surface_data_->height - SY); + + const Uint8* src = surface_data_->data.get(); + for (int iy = 0; iy < sh; ++iy) { + for (int ix = 0; ix < sw; ++ix) { + const int SRC_X = (flip == SDL_FLIP_HORIZONTAL) ? (SX + sw - 1 - ix) : (SX + ix); + const int SRC_Y = (flip == SDL_FLIP_VERTICAL) ? (SY + sh - 1 - iy) : (SY + iy); + const int DST_X = x + ix; + const int DST_Y = y + iy; + if (DST_X < 0 || DST_X >= DST_W || DST_Y < 0 || DST_Y >= DST_H) { continue; } + + const Uint8 COLOR = src[SRC_X + (SRC_Y * surface_data_->width)]; + if (COLOR == transparent_color_) { continue; } + const Uint8 REMAPPED = (COLOR == source_color) ? target_color : sub_palette_[COLOR]; + dst[DST_X + (DST_Y * DST_W)] = palette_[REMAPPED]; + } + } +} diff --git a/source/core/rendering/surface.hpp b/source/core/rendering/surface.hpp index 246d937..d9806c8 100644 --- a/source/core/rendering/surface.hpp +++ b/source/core/rendering/surface.hpp @@ -52,6 +52,10 @@ class Surface { // Vobla la regió indicada al framebuffer ARGB del joc usant la paleta propia. void render(int x, int y, const SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE) const; + // Igual que render() però substituint els píxels amb valor source_color per + // target_color abans d'aplicar la paleta. Útil per a la classe Text: els + // glifs guarden "tinta" amb un únic índex i es recoloren per paràmetre. + void renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 target_color, const SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE) const; void clear(Uint8 color); void putPixel(int x, int y, Uint8 color); @@ -62,7 +66,7 @@ class Surface { [[nodiscard]] auto getHeight() const -> int { return surface_data_->height; } [[nodiscard]] auto getTransparentColor() const -> Uint8 { return transparent_color_; } - void setTransparentColor(Uint8 color = 255) { transparent_color_ = color; } + void setTransparentColor(Uint8 color = 0) { transparent_color_ = color; } void setPalette(const Palette& palette) { palette_ = palette; } [[nodiscard]] auto getPaletteColor(Uint8 index) const -> Uint32 { return palette_[index]; } @@ -73,5 +77,8 @@ class Surface { std::shared_ptr surface_data_; Palette palette_{}; SubPalette sub_palette_{}; - Uint8 transparent_color_{255}; + // Convenció de tots els GIFs del projecte (sprites i fonts): l'índex 0 + // és el color de fons/transparent. Els tilesets que usen un altre índex + // han de cridar setTransparentColor() explícitament. + Uint8 transparent_color_{0}; }; diff --git a/source/core/rendering/text.cpp b/source/core/rendering/text.cpp new file mode 100644 index 0000000..bb6176e --- /dev/null +++ b/source/core/rendering/text.cpp @@ -0,0 +1,258 @@ +#include "core/rendering/text.hpp" + +#include + +#include +#include +#include +#include +#include + +#include "core/rendering/sprite/sprite.hpp" +#include "core/rendering/surface.hpp" +#include "core/resources/resource_helper.hpp" +#include "utils/utils.hpp" + +// Decodifica el pròxim codepoint UTF-8 de la cadena i avança 'pos' al byte +// següent. Retorna U+FFFD davant seqüències malformades perquè el caller +// seguisca dibuixant (cau al glif '?' més endavant). +auto Text::nextCodepoint(const std::string& s, std::size_t& pos) -> std::uint32_t { + auto c = static_cast(s[pos]); + std::uint32_t cp = 0; + std::size_t extra = 0; + + if (c < 0x80) { + cp = c; + extra = 0; + } else if (c < 0xC0 || c >= 0xF8) { + pos++; + return 0xFFFD; + } else if (c < 0xE0) { + cp = c & 0x1F; + extra = 1; + } else if (c < 0xF0) { + cp = c & 0x0F; + extra = 2; + } else { + cp = c & 0x07; + extra = 3; + } + + pos++; + for (std::size_t i = 0; i < extra && pos < s.size(); ++i, ++pos) { + auto cb = static_cast(s[pos]); + if ((cb & 0xC0) != 0x80) { return 0xFFFD; } + cp = (cp << 6) | (cb & 0x3F); + } + return cp; +} + +auto Text::codepointToUtf8(std::uint32_t cp) -> std::string { + std::string result; + if (cp < 0x80) { + result += static_cast(cp); + } else if (cp < 0x800) { + result += static_cast(0xC0 | (cp >> 6)); + result += static_cast(0x80 | (cp & 0x3F)); + } else if (cp < 0x10000) { + result += static_cast(0xE0 | (cp >> 12)); + result += static_cast(0x80 | ((cp >> 6) & 0x3F)); + result += static_cast(0x80 | (cp & 0x3F)); + } else { + result += static_cast(0xF0 | (cp >> 18)); + result += static_cast(0x80 | ((cp >> 12) & 0x3F)); + result += static_cast(0x80 | ((cp >> 6) & 0x3F)); + result += static_cast(0x80 | (cp & 0x3F)); + } + return result; +} + +// Format .fnt: claus "box_width/box_height/columns/cell_spacing/row_spacing" +// i una línia per glif amb "codepoint_decimal amplada_visual". Els glifs +// s'ordenen al png per ordre d'aparició al .fnt (glyph_index). +auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr { + auto tf = std::make_shared(); + + auto file_data = Resource::Helper::loadFile(file_path); + if (file_data.empty()) { + std::cerr << "Error: Fitxer no trobat " << getFileName(file_path) << '\n'; + throw std::runtime_error("Fitxer no trobat: " + getFileName(file_path)); + } + + std::string content(file_data.begin(), file_data.end()); + std::istringstream stream(content); + std::string line; + int glyph_index = 0; + + while (std::getline(stream, line)) { + if (!line.empty() && line.back() == '\r') { line.pop_back(); } + if (line.empty() || line[0] == '#') { continue; } + + std::istringstream ls(line); + std::string key; + ls >> key; + + if (key == "box_width") { + ls >> tf->box_width; + } else if (key == "box_height") { + ls >> tf->box_height; + } else if (key == "columns") { + ls >> tf->columns; + } else if (key == "cell_spacing") { + ls >> tf->cell_spacing; + } else if (key == "row_spacing") { + ls >> tf->row_spacing; + } else { + std::uint32_t codepoint = 0; + int width = 0; + try { + codepoint = static_cast(std::stoul(key)); + ls >> width; + } catch (...) { + continue; + } + Offset off{}; + const int ROW_SP = tf->row_spacing > 0 ? tf->row_spacing : tf->cell_spacing; + off.x = ((glyph_index % tf->columns) * (tf->box_width + tf->cell_spacing)) + tf->cell_spacing; + off.y = ((glyph_index / tf->columns) * (tf->box_height + ROW_SP)) + tf->cell_spacing; + off.w = width; + tf->offset[codepoint] = off; + ++glyph_index; + } + } + + printWithDots("Text File : ", getFileName(file_path), "[ LOADED ]"); + return tf; +} + +Text::Text(const std::shared_ptr& surface, const std::string& text_file) { + auto tf = loadTextFile(text_file); + box_height_ = tf->box_height; + box_width_ = tf->box_width; + offset_ = tf->offset; + sprite_ = std::make_unique(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast(box_width_), .h = static_cast(box_height_)}); +} + +Text::Text(const std::shared_ptr& surface, const std::shared_ptr& text_file) + : sprite_(std::make_unique(surface, SDL_FRect{.x = 0.0F, .y = 0.0F, .w = static_cast(text_file->box_width), .h = static_cast(text_file->box_height)})), + box_width_(text_file->box_width), + box_height_(text_file->box_height), + offset_(text_file->offset) { +} + +// Dibuixa la cadena amb la tinta per defecte (codi 15 a la paleta del png). +// Els glifs amb codepoint no registrat es substitueixen pel glif '?'. +void Text::write(int x, int y, const std::string& text, int kerning, int length) { + int shift = 0; + int glyphs_done = 0; + std::size_t pos = 0; + + sprite_->setY(static_cast(y)); + while (pos < text.size()) { + if (length != -1 && glyphs_done >= length) { break; } + std::uint32_t cp = nextCodepoint(text, pos); + auto it = offset_.find(cp); + if (it == offset_.end()) { it = offset_.find('?'); } + if (it != offset_.end()) { + sprite_->setClip(static_cast(it->second.x), static_cast(it->second.y), static_cast(box_width_), static_cast(box_height_)); + sprite_->setX(static_cast(x + shift)); + sprite_->render(1, 15); + shift += it->second.w + kerning; + } + ++glyphs_done; + } +} + +void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int kerning, int length) { + int shift = 0; + int glyphs_done = 0; + std::size_t pos = 0; + + sprite_->setY(static_cast(y)); + while (pos < text.size()) { + if (length != -1 && glyphs_done >= length) { break; } + std::uint32_t cp = nextCodepoint(text, pos); + auto it = offset_.find(cp); + if (it == offset_.end()) { it = offset_.find('?'); } + if (it != offset_.end()) { + sprite_->setClip(static_cast(it->second.x), static_cast(it->second.y), static_cast(box_width_), static_cast(box_height_)); + sprite_->setX(static_cast(x + shift)); + sprite_->render(1, color); + shift += it->second.w + kerning; + } + ++glyphs_done; + } +} + +void Text::writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance, int kerning, int length) { + writeColored(x + shadow_distance, y + shadow_distance, text, color, kerning, length); + write(x, y, text, kerning, length); +} + +void Text::writeCentered(int x, int y, const std::string& text, int kerning, int length) { + x -= (Text::length(text, kerning) / 2); + write(x, y, text, kerning, length); +} + +void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int length) { + const bool CENTERED = ((flags & CENTER_FLAG) == CENTER_FLAG); + const bool SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG); + const bool COLORED = ((flags & COLOR_FLAG) == COLOR_FLAG); + const bool STROKED = ((flags & STROKE_FLAG) == STROKE_FLAG); + + if (CENTERED) { x -= (Text::length(text, kerning) / 2); } + + if (SHADOWED) { + writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, length); + } + + if (STROKED) { + const int MAX_DIST = static_cast(shadow_distance); + for (int dist = 1; dist <= MAX_DIST; ++dist) { + for (int dy = -dist; dy <= dist; ++dy) { + for (int dx = -dist; dx <= dist; ++dx) { + writeColored(x + dx, y + dy, text, shadow_color, kerning, length); + } + } + } + } + + if (COLORED) { + writeColored(x, y, text, text_color, kerning, length); + } else { + write(x, y, text, kerning, length); + } +} + +auto Text::length(const std::string& text, int kerning) const -> int { + int shift = 0; + std::size_t pos = 0; + + while (pos < text.size()) { + std::uint32_t cp = nextCodepoint(text, pos); + auto it = offset_.find(cp); + if (it == offset_.end()) { it = offset_.find('?'); } + if (it != offset_.end()) { + shift += it->second.w + kerning; + } + } + + return shift > 0 ? shift - kerning : 0; +} + +auto Text::glyphWidth(std::uint32_t codepoint, int kerning) const -> int { + auto it = offset_.find(codepoint); + if (it == offset_.end()) { it = offset_.find('?'); } + if (it != offset_.end()) { return it->second.w + kerning; } + return 0; +} + +auto Text::getGlyphClip(std::uint32_t codepoint) const -> SDL_FRect { + auto it = offset_.find(codepoint); + if (it == offset_.end()) { it = offset_.find('?'); } + if (it == offset_.end()) { return {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; } + return {.x = static_cast(it->second.x), + .y = static_cast(it->second.y), + .w = static_cast(box_width_), + .h = static_cast(box_height_)}; +} diff --git a/source/core/rendering/text.hpp b/source/core/rendering/text.hpp new file mode 100644 index 0000000..3b283a4 --- /dev/null +++ b/source/core/rendering/text.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "core/rendering/sprite/sprite.hpp" +class Surface; + +// Bitmap font amb suport UTF-8. Els glifs es guarden en una única graella +// al png, amb ample variable per caràcter i una tinta monocroma que es +// recolora al vol via Sprite::render(src, tgt). +class Text { + public: + struct Offset { + int x{0}; + int y{0}; + int w{0}; + }; + + struct File { + int box_width{0}; + int box_height{0}; + int columns{16}; + int cell_spacing{0}; + int row_spacing{0}; + std::unordered_map offset; + }; + + // Flags per a writeDX (composables amb |). + static constexpr Uint8 COLOR_FLAG = 1; + static constexpr Uint8 SHADOW_FLAG = 2; + static constexpr Uint8 CENTER_FLAG = 4; + static constexpr Uint8 STROKE_FLAG = 8; + + Text(const std::shared_ptr& surface, const std::string& text_file); + Text(const std::shared_ptr& surface, const std::shared_ptr& text_file); + ~Text() = default; + + Text(const Text&) = delete; + Text(Text&&) = delete; + auto operator=(const Text&) -> Text& = delete; + auto operator=(Text&&) -> Text& = delete; + + void write(int x, int y, const std::string& text, int kerning = 1, int length = -1); + void writeColored(int x, int y, const std::string& text, Uint8 color, int kerning = 1, int length = -1); + void writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance = 1, int kerning = 1, int length = -1); + void writeCentered(int x, int y, const std::string& text, int kerning = 1, int length = -1); + void writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning = 1, Uint8 text_color = 0, Uint8 shadow_distance = 1, Uint8 shadow_color = 0, int length = -1); + + [[nodiscard]] auto length(const std::string& text, int kerning = 1) const -> int; + [[nodiscard]] auto getCharacterSize() const -> int { return box_width_; } + [[nodiscard]] auto glyphWidth(std::uint32_t codepoint, int kerning = 0) const -> int; + [[nodiscard]] auto getGlyphClip(std::uint32_t codepoint) const -> SDL_FRect; + [[nodiscard]] auto getSprite() const -> Sprite* { return sprite_.get(); } + + static auto loadTextFile(const std::string& file_path) -> std::shared_ptr; + static auto codepointToUtf8(std::uint32_t cp) -> std::string; + static auto nextCodepoint(const std::string& s, std::size_t& pos) -> std::uint32_t; + + private: + std::unique_ptr sprite_; + int box_width_{0}; + int box_height_{0}; + std::unordered_map offset_; +}; diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 705befb..13e56c9 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -10,6 +10,7 @@ #include #include +#include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" #include "core/resources/resource_cache.hpp" #include "core/scene/scene.hpp" @@ -51,6 +52,10 @@ Director::Director() { Screen::get().applyCrtPiParams(Options::buildCrtPiParams()); (void)Resource::Cache::get(); + // L'overlay de debug necessita Screen inicialitzat (llegirà el canvas + // i carregarà la font 8bithud via Resource::Cache). + RenderInfo::get().init(); + current_scene_ = std::make_unique(); last_ticks_ms_ = SDL_GetTicks(); } diff --git a/source/game/options.cpp b/source/game/options.cpp index 0a9e0e3..7ec04d7 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -97,6 +97,7 @@ namespace { if (node.contains("texture_filter")) { Options::video.texture_filter = filterFromString(node["texture_filter"].get_value()); } + if (node.contains("render_info")) { Options::video.render_info = node["render_info"].get_value(); } parseGpu(node); parseShader(node); } @@ -285,6 +286,7 @@ namespace Options { out << " vsync: " << (video.vsync ? "true" : "false") << "\n"; out << " scaling_mode: \"" << scalingModeToString(video.scaling_mode) << "\"\n"; out << " texture_filter: \"" << filterToString(video.texture_filter) << "\"\n"; + out << " render_info: " << (video.render_info ? "true" : "false") << "\n"; out << " gpu:\n"; out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n"; out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n"; diff --git a/source/game/options.hpp b/source/game/options.hpp index 4751531..06cb32b 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -34,6 +34,7 @@ namespace Options { bool vsync{Defaults::Video::VSYNC}; Defaults::ScalingMode scaling_mode{Defaults::Video::SCALING_MODE}; Defaults::TextureFilter texture_filter{Defaults::Video::TEXTURE_FILTER}; + bool render_info{Defaults::Video::RENDER_INFO}; Gpu gpu{}; Shader shader{}; }; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index d3e3c15..99d105a 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -2,6 +2,7 @@ #include +#include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" #include "core/resources/resource_cache.hpp" #include "core/system/event_buffer.hpp" @@ -26,17 +27,20 @@ GameScene::~GameScene() = default; void GameScene::update(int delta_ms) { for (const auto& event : EventBuffer::events) { - if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE) { - wants_quit_ = true; + if (event.type == SDL_EVENT_KEY_DOWN) { + if (event.key.scancode == SDL_SCANCODE_ESCAPE) { wants_quit_ = true; } + if (event.key.scancode == SDL_SCANCODE_F3) { RenderInfo::get().toggle(); } } } const float DT = static_cast(delta_ms) / 1000.0F; player_->update(DT); + RenderInfo::get().update(delta_ms); } void GameScene::render() { Screen::get().clear(0x00, 0x00, 0x00); player_->render(); + RenderInfo::get().render(); Screen::get().present(); } diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index eb4a0db..4934699 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -40,6 +40,7 @@ namespace Defaults { constexpr const char* CRTPI_DEFAULT_PRESET = "default"; // Preset inicial CrtPi. constexpr const char* POSTFX_FILE = "shaders/postfx.yaml"; // Relatiu a la carpeta de sistema. constexpr const char* CRTPI_FILE = "shaders/crtpi.yaml"; // Relatiu a la carpeta de sistema. + constexpr bool RENDER_INFO = false; // Overlay de debug (FPS/driver/shader) amagat per defecte. } // namespace Video namespace Audio { diff --git a/source/utils/defines.hpp b/source/utils/defines.hpp index 0c1b078..b488ada 100644 --- a/source/utils/defines.hpp +++ b/source/utils/defines.hpp @@ -3,7 +3,7 @@ namespace Texts { constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner"; constexpr const char* COPYRIGHT = "© 2026 JailDesigner"; - constexpr const char* VERSION = "0.6.0"; // Single source of truth — Makefile el grep + constexpr const char* VERSION = "0.7.0"; // Single source of truth — Makefile el grep } // namespace Texts namespace GameCanvas {