From 87b08a7c7c44d03b105820d5b882c95b492bfa70 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Thu, 23 Apr 2026 13:17:47 +0200 Subject: [PATCH] =?UTF-8?q?feat(map):=20afegeix=20PaintLayer=20com=20a=20f?= =?UTF-8?q?ont=20=C3=BAnica=20del=20pintat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 1 + source/game/map/paint_layer.cpp | 134 ++++++++++++++++++++++++++++++++ source/game/map/paint_layer.hpp | 72 +++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 source/game/map/paint_layer.cpp create mode 100644 source/game/map/paint_layer.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d0b61f..7efa628 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,6 +93,7 @@ set(APP_SOURCES source/game/map/map.cpp source/game/map/tilemap.cpp source/game/map/tilemap_writer.cpp + source/game/map/paint_layer.cpp # Mòdul editor (només activa contingut en Debug via `#ifdef _DEBUG`). # En Release el fitxer compila a un object buit i l'enllaçador no # arrossega cap símbol del joc final. diff --git a/source/game/map/paint_layer.cpp b/source/game/map/paint_layer.cpp new file mode 100644 index 0000000..35be755 --- /dev/null +++ b/source/game/map/paint_layer.cpp @@ -0,0 +1,134 @@ +#include "game/map/paint_layer.hpp" + +#include +#include +#include +#include +#include + +#include "core/rendering/screen.hpp" +#include "core/rendering/surface.hpp" +#include "core/resources/resource_cache.hpp" +#include "game/map/map.hpp" +#include "game/map/tilemap.hpp" + +namespace { + + // Blit d'un tile del tileset sobre un buffer ARGB de destí, respectant + // l'índex transparent del GIF. Mateixa convenció que la versió a + // `tilemap.cpp`: el tileset guarda índexs + paleta, i la destinació + // és un buffer ARGB32 lineal. Duplicat intencionat per no acoblar + // Tilemap i PaintLayer; si en el futur un tercer consumidor el vol + // reutilitzar, es promou a un helper compartit. + void blitTile(const Surface& tileset, int tile_size, int src_col, int src_row, std::vector& dest, int dest_w, int dest_x, int dest_y) { + const int SRC_X0 = src_col * tile_size; + const int SRC_Y0 = src_row * tile_size; + const auto TRANSPARENT = tileset.getTransparentColor(); + for (int y = 0; y < tile_size; ++y) { + for (int x = 0; x < tile_size; ++x) { + const auto IDX = tileset.getPixel(SRC_X0 + x, SRC_Y0 + y); + if (IDX == TRANSPARENT) { continue; } + const auto ARGB = tileset.getPaletteColor(IDX); + const auto DST_OFFSET = + (static_cast(dest_y + y) * static_cast(dest_w)) + static_cast(dest_x + x); + dest[DST_OFFSET] = ARGB; + } + } + } + +} // namespace + +PaintLayer::PaintLayer(const Tilemap& tilemap, EventBus* bus) + : bus_(bus), + cols_(tilemap.getCols()), + rows_(tilemap.getRows()), + tile_size_(tilemap.getTileSize()), + origin_x_(tilemap.getOriginX()), + origin_y_(tilemap.getOriginY()), + width_px_(tilemap.getWidth()), + height_px_(tilemap.getHeight()), + tileset_cols_(tilemap.getTilesetCols()), + tileset_path_(tilemap.getTilesetPath()) { + painted_.assign(static_cast(rows_), std::vector(static_cast(cols_), -1)); + pixels_.assign( + static_cast(width_px_) * static_cast(height_px_), + 0U); + + if (bus_ != nullptr) { + // Subscripció a `on_tile_entered`: el Player publica aquest event + // amb `PaintedTile{col, row, tile_index}`. Filtrem pintabilitat + // (sòl transitable) via `Map::isPaintable` i, si aplica, marquem + // i re-publiquem `tile_painted` amb només la posició. + on_tile_sub_ = bus_->subscribe("on_tile_entered", [this](const Event& ev) { + const auto* paint = std::get_if(&ev.payload); + if (paint != nullptr) { tryPaint(paint->col, paint->row, paint->tile_index); } + }); + } + + std::cout << "PaintLayer: " << cols_ << "x" << rows_ << " tiles (" + << width_px_ << "x" << height_px_ << " px), tileset " + << tileset_path_ << '\n'; +} + +PaintLayer::~PaintLayer() { + if (bus_ != nullptr && on_tile_sub_ != 0) { bus_->unsubscribe(on_tile_sub_); } +} + +auto PaintLayer::isPainted(int col, int row) const -> bool { + if (col < 0 || col >= cols_ || row < 0 || row >= rows_) { return false; } + return painted_[static_cast(row)][static_cast(col)] >= 0; +} + +auto PaintLayer::tryPaint(int col, int row, int tile_index) -> bool { + if (col < 0 || col >= cols_ || row < 0 || row >= rows_) { return false; } + if (tile_index < 0) { return false; } + if (painted_[static_cast(row)][static_cast(col)] >= 0) { + // Primer que pinta guanya: no sobreescrivim. + return false; + } + if (!Map::isPaintable(col, row)) { return false; } + + painted_[static_cast(row)][static_cast(col)] = tile_index; + blitPaintTile(col, row, tile_index); + + if (bus_ != nullptr) { + bus_->publish("tile_painted", TilePos{.col = col, .row = row}); + } + return true; +} + +void PaintLayer::blitPaintTile(int col, int row, int tile_index) { + const auto TILESET = Resource::Cache::get().getSurface(tileset_path_); + if (TILESET == nullptr) { return; } + const int DX = col * tile_size_; + const int DY = row * tile_size_; + const int SRC_COL = tile_index % tileset_cols_; + const int SRC_ROW = tile_index / tileset_cols_; + blitTile(*TILESET, tile_size_, SRC_COL, SRC_ROW, pixels_, width_px_, DX, DY); +} + +void PaintLayer::render() const { + // Còpia ARGB al canvas, anàloga a `Tilemap::render`. Píxels amb + // alpha=0 (tiles no pintats) es salten per deixar visible el tilemap + // base per sota. + auto& screen = Screen::get(); + auto* canvas = screen.getCanvasArgb(); + const int CANVAS_W = screen.getCanvasWidth(); + const int CANVAS_H = screen.getCanvasHeight(); + + const int Y_START = std::max(0, -origin_y_); + const int Y_END = std::min(height_px_, CANVAS_H - origin_y_); + const int X_START = std::max(0, -origin_x_); + const int X_END = std::min(width_px_, CANVAS_W - origin_x_); + + constexpr std::uint32_t ALPHA_MASK = 0xFF000000U; + for (int y = Y_START; y < Y_END; ++y) { + const auto* src_row = &pixels_[static_cast(y) * static_cast(width_px_)]; + auto* dst_row = + &canvas[(static_cast(origin_y_ + y) * static_cast(CANVAS_W)) + static_cast(origin_x_)]; + for (int x = X_START; x < X_END; ++x) { + const auto PIXEL = src_row[x]; + if ((PIXEL & ALPHA_MASK) != 0U) { dst_row[x] = PIXEL; } + } + } +} diff --git a/source/game/map/paint_layer.hpp b/source/game/map/paint_layer.hpp new file mode 100644 index 0000000..4088ef8 --- /dev/null +++ b/source/game/map/paint_layer.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include + +#include "game/entities/event_bus.hpp" + +class Tilemap; + +// Capa visual de "pintat" sobre el tilemap base. Quan el jugador (o +// qualsevol altre actor futur) publica `on_tile_entered` amb payload +// `PaintedTile{col, row, tile_index}`, el PaintLayer filtra per +// `Map::isPaintable`, marca el tile i re-publica `tile_painted` amb +// només `TilePos` — abstracció del "el tile s'ha pintat" independent de +// qui ho ha fet. Els sarcòfags escolten aquest segon event per comptar +// el seu perímetre. +// +// Font única de veritat del pintat: evita duplicar l'estat dins de cada +// sarcòfag i permet que el render mostri l'efecte al instant. +// +// Geometria: hereta `tile_size`, origen i dimensions del Tilemap +// (mateixa graella). Reutilitza el tileset del Tilemap per blitejar els +// tiles pintats a un buffer ARGB propi, anàleg a `Tilemap::pixels_`. +// +// Política de repintat: el primer que pinta un tile guanya. Intents +// posteriors (amb el mateix índex o un altre) no tenen efecte — +// garanteix coherència visual i evita notificar dos cops al mateix sarc. +class PaintLayer { + public: + PaintLayer(const Tilemap& tilemap, EventBus* bus); + ~PaintLayer(); + + PaintLayer(const PaintLayer&) = delete; + PaintLayer(PaintLayer&&) = delete; + auto operator=(const PaintLayer&) -> PaintLayer& = delete; + auto operator=(PaintLayer&&) -> PaintLayer& = delete; + + // Còpia del buffer ARGB sobre el canvas del Screen, respectant alpha + // (píxels no pintats queden al buffer amb alpha=0 i no s'escriuen). + // Pensat per cridar entre `Tilemap::render()` i el bloc d'actors. + void render() const; + + // Intent de pintat directe (exposat per a tests i actors que pinten + // sense passar pel bus). Retorna true si el tile és pintable i no + // estava ja pintat; en aquest cas, publica `tile_painted`. + auto tryPaint(int col, int row, int tile_index) -> bool; + + [[nodiscard]] auto isPainted(int col, int row) const -> bool; + + private: + void blitPaintTile(int col, int row, int tile_index); + + EventBus* bus_{nullptr}; + EventBus::SubId on_tile_sub_{0}; + + int cols_{0}; + int rows_{0}; + int tile_size_{0}; + int origin_x_{0}; + int origin_y_{0}; + int width_px_{0}; + int height_px_{0}; + int tileset_cols_{0}; + std::string tileset_path_; + + // `-1` = no pintat. Altres valors = índex de tile del tileset usat + // per pintar. No cal per al render (el buffer `pixels_` ja el conté), + // però facilita consultes ràpides d'estat i futures extensions. + std::vector> painted_; + std::vector pixels_; +};