Acaba la migració dels std::cout/cerr a Log::*:
- core/locale, core/ui/ui_config, core/rendering/{screen,text,gif,
surface, sprite/animated_sprite, sdl3gpu/*}.
- game/{options, debug_config, editor/editor_config, levels/level_manager,
map/{map,tilemap,paint_layer}, entities/entity_manager_loadlevel}.
Severitats coherents: warns per a configs absents amb fallback,
errors per a YAML malformat o E/S fallida, infos per a estats.
Resultat:
- 0 occurrences de std::cout/std::cerr a source/.
- Cap #include <iostream> orfà (eliminat de surface.cpp).
- Output funcional idèntic al històric; AEE_LOG filtra (ex.
`AEE_LOG=warn` només mostra warnings, `AEE_LOG=*:warn,Tag:info`
combina globals i overrides).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
168 lines
7.2 KiB
C++
168 lines
7.2 KiB
C++
#include "game/map/paint_layer.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <variant>
|
|
|
|
#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"
|
|
#include "utils/log.hpp"
|
|
|
|
// CONTRACTE BUS DE PINTAT
|
|
// -----------------------
|
|
// `on_tile_entered` — senyal FÍSICA: un jugador acaba d'entrar a un tile.
|
|
// La publica `Player::publishTileEnteredIfChanged`. La consumeix aquest
|
|
// `PaintLayer` per decidir si renderitzar el tile com a pintat (filtra
|
|
// per `Map::isPaintable` i aplica la regla "primer que pinta guanya").
|
|
//
|
|
// `tile_painted` — senyal LÒGICA: un tile compta com a pintat per a la
|
|
// lògica de joc (sarcòfags i, en el futur, HUD %completat, events
|
|
// derivats). La publica el `PaintLayer` quan confirma un pintat físic,
|
|
// i també la publica `EntityManager::loadFromLevel` com a broadcast
|
|
// inicial per als tiles ocupats per decals de sòl (Hole, etc.) — que
|
|
// el jugador mai podrà trepitjar perquè cauria al forat i els
|
|
// sarcòfags vecins necessiten que aquests tiles comptin com a pintats
|
|
// per completar el perímetre.
|
|
//
|
|
// REGLA: NO connectar `PaintLayer` al canal `tile_painted` per
|
|
// renderitzar. Si ho féssim, els broadcasts inicials de decals pintarien
|
|
// visualment els tiles d'agujero, incoherent amb la representació
|
|
// gràfica del decal.
|
|
|
|
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<std::uint32_t>& 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<std::size_t>(dest_y + y) * static_cast<std::size_t>(dest_w)) + static_cast<std::size_t>(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<std::size_t>(rows_), std::vector<int>(static_cast<std::size_t>(cols_), -1));
|
|
pixels_.assign(
|
|
static_cast<std::size_t>(width_px_) * static_cast<std::size_t>(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) {
|
|
// Re-propaguem el `player_id` de qui ha trepitjat la cel·la, perquè
|
|
// `tile_painted` (que consumeixen els sarcòfags i el HUD futur)
|
|
// sàpiga quin jugador ha pintat.
|
|
const auto* paint = std::get_if<PaintedTile>(&ev.payload);
|
|
if (paint != nullptr) { tryPaint(paint->col, paint->row, paint->tile_index, ev.player_id); }
|
|
});
|
|
}
|
|
|
|
Log::info("PaintLayer", "{}x{} tiles ({}x{} px), tileset {}", cols_, rows_, width_px_, height_px_, tileset_path_);
|
|
}
|
|
|
|
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<std::size_t>(row)][static_cast<std::size_t>(col)] >= 0;
|
|
}
|
|
|
|
void PaintLayer::clear() {
|
|
// Mateixa geometria que al ctor: matriu de tiles a -1 (no pintat) i
|
|
// buffer ARGB a zero (alpha=0 = transparent → render no escriu res).
|
|
painted_.assign(static_cast<std::size_t>(rows_), std::vector<int>(static_cast<std::size_t>(cols_), -1));
|
|
std::ranges::fill(pixels_, 0U);
|
|
}
|
|
|
|
auto PaintLayer::tryPaint(int col, int row, int tile_index, int player_id) -> bool {
|
|
if (col < 0 || col >= cols_ || row < 0 || row >= rows_) { return false; }
|
|
if (tile_index < 0) { return false; }
|
|
if (painted_[static_cast<std::size_t>(row)][static_cast<std::size_t>(col)] >= 0) {
|
|
// Primer que pinta guanya: no sobreescrivim.
|
|
return false;
|
|
}
|
|
if (!Map::isPaintable(col, row)) { return false; }
|
|
|
|
painted_[static_cast<std::size_t>(row)][static_cast<std::size_t>(col)] = tile_index;
|
|
blitPaintTile(col, row, tile_index);
|
|
|
|
if (bus_ != nullptr) {
|
|
bus_->publish(Event{
|
|
.name = "tile_painted",
|
|
.payload = TilePos{.col = col, .row = row},
|
|
.player_id = player_id,
|
|
});
|
|
}
|
|
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<std::size_t>(y) * static_cast<std::size_t>(width_px_)];
|
|
auto* dst_row =
|
|
&canvas[(static_cast<std::size_t>(origin_y_ + y) * static_cast<std::size_t>(CANVAS_W)) + static_cast<std::size_t>(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; }
|
|
}
|
|
}
|
|
}
|