174 lines
7.3 KiB
C++
174 lines
7.3 KiB
C++
#include "game/map/tilemap.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <stdexcept>
|
|
#include <vector>
|
|
|
|
#include "core/rendering/screen.hpp"
|
|
#include "core/rendering/surface.hpp"
|
|
#include "core/resources/resource_cache.hpp"
|
|
#include "external/fkyaml_node.hpp"
|
|
#include "game/map/room_yaml.hpp"
|
|
#include "utils/log.hpp"
|
|
|
|
#ifdef _DEBUG
|
|
#include "game/map/tilemap_writer.hpp"
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
// Copia un tile (col, fila) del tileset al buffer ARGB a la posició de
|
|
// destí (dest_x, dest_y). Els píxels amb l'índex de transparència del
|
|
// tileset es deixen com estan al buffer (alpha=0 per defecte).
|
|
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
|
|
|
|
Tilemap::Tilemap(const std::string& yaml_path) {
|
|
auto yaml = RoomYaml::loadMerged(yaml_path);
|
|
|
|
tileset_path_ = yaml["tileset"].get_value<std::string>();
|
|
tile_size_ = yaml["tile_size"].get_value<int>();
|
|
if (yaml.contains("origin")) {
|
|
origin_x_ = yaml["origin"][0].get_value<int>();
|
|
origin_y_ = yaml["origin"][1].get_value<int>();
|
|
}
|
|
|
|
// Carreguem el tileset al cache (idempotent) i el pillem per accedir
|
|
// als seus píxels i paleta. La paleta del tileset ve del GIF mateix.
|
|
// Ens cal abans de llegir `data` per saber quants tiles per fila té.
|
|
Resource::Cache::get().loadSurface(tileset_path_, tileset_path_);
|
|
const auto TILESET = Resource::Cache::get().getSurface(tileset_path_);
|
|
tileset_cols_ = TILESET->getWidth() / tile_size_;
|
|
|
|
// `data` és una seqüència de seqüències d'enters: cada fila és una
|
|
// llista d'índexs lineals al tileset (esq→dre, dalt→baix). Un valor
|
|
// negatiu (per convenció -1) deixa la cel·la buida (alpha=0).
|
|
const auto& data = yaml["data"];
|
|
if (!data.is_sequence() || data.empty()) {
|
|
throw std::runtime_error("Tilemap: `data` buit o amb format incorrecte a " + yaml_path);
|
|
}
|
|
rows_ = static_cast<int>(data.size());
|
|
cols_ = static_cast<int>(data[0].size());
|
|
width_px_ = cols_ * tile_size_;
|
|
height_px_ = rows_ * tile_size_;
|
|
pixels_.assign(
|
|
static_cast<std::size_t>(width_px_) * static_cast<std::size_t>(height_px_),
|
|
0U);
|
|
|
|
#ifdef _DEBUG
|
|
data_.assign(static_cast<std::size_t>(rows_), std::vector<int>(static_cast<std::size_t>(cols_), -1));
|
|
#endif
|
|
|
|
for (int row = 0; row < rows_; ++row) {
|
|
const auto& line = data[row];
|
|
const int LINE_COLS = static_cast<int>(line.size());
|
|
for (int col = 0; col < cols_ && col < LINE_COLS; ++col) {
|
|
const int INDEX = line[col].get_value<int>();
|
|
#ifdef _DEBUG
|
|
data_[static_cast<std::size_t>(row)][static_cast<std::size_t>(col)] = INDEX;
|
|
#endif
|
|
if (INDEX < 0) { continue; }
|
|
const int SRC_COL = INDEX % tileset_cols_;
|
|
const int SRC_ROW = INDEX / tileset_cols_;
|
|
blitTile(*TILESET, tile_size_, SRC_COL, SRC_ROW, pixels_, width_px_, col * tile_size_, row * tile_size_);
|
|
}
|
|
}
|
|
|
|
Log::info("Tilemap", "{}x{} tiles ({}x{} px) des de {}", cols_, rows_, width_px_, height_px_, yaml_path);
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
void Tilemap::setTileAt(int col, int row, int index) {
|
|
if (col < 0 || col >= cols_ || row < 0 || row >= rows_) { return; }
|
|
if (index < 0) { return; } // El mapa no admet cel·les buides (política de projecte).
|
|
// No validem l'índex contra el total de tiles del tileset: la
|
|
// invariant la garanteix el TilePicker, que només ofereix índexs en
|
|
// rang.
|
|
|
|
data_[static_cast<std::size_t>(row)][static_cast<std::size_t>(col)] = index;
|
|
|
|
// Netegem el rectangle del tile al buffer de píxels i re-blitegem.
|
|
const int DX = col * tile_size_;
|
|
const int DY = row * tile_size_;
|
|
for (int y = 0; y < tile_size_; ++y) {
|
|
auto* dst = &pixels_[(static_cast<std::size_t>(DY + y) * static_cast<std::size_t>(width_px_)) + static_cast<std::size_t>(DX)];
|
|
for (int x = 0; x < tile_size_; ++x) { dst[x] = 0U; }
|
|
}
|
|
const auto TILESET = Resource::Cache::get().getSurface(tileset_path_);
|
|
const int SRC_COL = index % tileset_cols_;
|
|
const int SRC_ROW = index / tileset_cols_;
|
|
blitTile(*TILESET, tile_size_, SRC_COL, SRC_ROW, pixels_, width_px_, DX, DY);
|
|
}
|
|
|
|
auto Tilemap::save(const std::string& yaml_path) const -> bool {
|
|
return TilemapWriter::saveDataBlock(yaml_path, data_);
|
|
}
|
|
|
|
void Tilemap::renderTilePreview(int canvas_x, int canvas_y, int tile_index) const {
|
|
if (tile_index < 0) { return; }
|
|
const auto TILESET = Resource::Cache::get().getSurface(tileset_path_);
|
|
if (TILESET == nullptr) { return; }
|
|
auto& screen = Screen::get();
|
|
auto* buf = screen.getCanvasArgb();
|
|
const int CW = screen.getCanvasWidth();
|
|
const int CH = screen.getCanvasHeight();
|
|
const auto TRANSPARENT = TILESET->getTransparentColor();
|
|
const int SX0 = (tile_index % tileset_cols_) * tile_size_;
|
|
const int SY0 = (tile_index / tileset_cols_) * tile_size_;
|
|
for (int y = 0; y < tile_size_; ++y) {
|
|
const int DY = canvas_y + y;
|
|
if (DY < 0 || DY >= CH) { continue; }
|
|
for (int x = 0; x < tile_size_; ++x) {
|
|
const int DX = canvas_x + x;
|
|
if (DX < 0 || DX >= CW) { continue; }
|
|
const auto IDX = TILESET->getPixel(SX0 + x, SY0 + y);
|
|
if (IDX == TRANSPARENT) { continue; }
|
|
buf[(static_cast<std::size_t>(DY) * static_cast<std::size_t>(CW)) + static_cast<std::size_t>(DX)] = TILESET->getPaletteColor(IDX);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void Tilemap::render() const {
|
|
auto& screen = Screen::get();
|
|
auto* canvas = screen.getCanvasArgb();
|
|
const int CANVAS_W = screen.getCanvasWidth();
|
|
const int CANVAS_H = screen.getCanvasHeight();
|
|
|
|
// Clipping de files: només pintem les que cauen dins del canvas.
|
|
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_);
|
|
|
|
// Màscara del canal alfa (els píxels transparents del tilemap queden
|
|
// amb alpha=0 i cal mantenir el canvas sota visible).
|
|
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; }
|
|
}
|
|
}
|
|
}
|