diff --git a/CMakeLists.txt b/CMakeLists.txt index 70c30c6..c1c1a9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,7 @@ set(APP_SOURCES source/game/scenes/game_scene.cpp source/game/entities/player.cpp source/game/map/map.cpp + source/game/map/tilemap.cpp ) find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3) diff --git a/data/maps/pyramid.yaml b/data/maps/pyramid.yaml new file mode 100644 index 0000000..6fc0a90 --- /dev/null +++ b/data/maps/pyramid.yaml @@ -0,0 +1,49 @@ +# Mapa de la piràmide: graella 21×15 de tiles 16×16 sobre tomb.gif. +# Overscan horitzontal: 8 px per banda (les parets laterals ocupen 2 tiles +# en el tilemap però només 1.5 visibles al canvas, així que la textura es +# dibuixa amb un origen de x=-8). Vertical no cal overscan: 15·16 = 240. +# +# Símbols del grid: +# W = paret (tile 99 del tileset, o (3,3) en 0-indexed) +# F = sòl (llista = un tile aleatori entre aquests per cada casella) +# . = cap tile (els sarcòfags seran actors, ací deixem transparent) +# +# Índexs lineals: els tiles van d'esquerra a dreta, de dalt a baix. Amb un +# tileset de 512×256 i tiles de 16×16 hi ha 32 tiles per fila. + +tileset: data/tilesets/tomb.gif +tile_size: 16 +origin: [-8, 0] + +legend: + W: 292 + X: 169 + Y: 1 + Z: 202 + T: 200 + R: 262 + G: 263 + H: 230 + I: 231 + J: 233 + A: 2 + B: 34 + C: 33 + D: 32 + +data: | + YYYYYYYYYYYYYYYYYYYYY + YHJJJJJJJJJJJJJJJJJIY + YZWWWWWWWWWWWWWWWWWTY + YZDDADDBBDDADDDDBDDTY + YZD...D...D...B...DTY + YZDDDCCDDDDDADDDBDDTY + YZD...D...A...D...DTY + YZDDCCDDDDDADDBDDDDTY + YZD...D...D...D...DTY + YZDDDDCCDDDDADDDDDDTY + YZD...D...D...D...DTY + YZDDDABDDDDADCDDBDDTY + YZD...D...D...D...DTY + YZDDBBDDADDDBCDDDDDTY + YRXXXXXXXXXXXXXXXXXGY diff --git a/data/palettes/srb2.pal b/data/palettes/srb2.pal new file mode 100644 index 0000000..110eace --- /dev/null +++ b/data/palettes/srb2.pal @@ -0,0 +1,259 @@ +JASC-PAL +0100 +256 +255 255 255 +247 247 247 +239 239 239 +231 231 231 +223 223 223 +215 215 215 +207 207 207 +199 199 199 +191 191 191 +183 183 183 +175 175 175 +167 167 167 +159 159 159 +151 151 151 +143 143 143 +135 135 135 +127 127 127 +119 119 119 +111 111 111 +103 103 103 +95 95 95 +87 87 87 +79 79 79 +71 71 71 +63 63 63 +55 55 55 +47 47 47 +39 39 39 +31 31 31 +23 23 23 +15 15 15 +7 7 7 +0 0 0 +191 167 143 +183 160 136 +175 152 128 +167 144 120 +159 137 113 +150 129 105 +142 121 97 +134 114 90 +126 106 82 +117 98 74 +109 90 66 +101 83 59 +93 75 51 +84 67 43 +76 60 36 +67 51 27 +191 123 75 +179 115 71 +171 111 67 +163 107 63 +155 99 59 +143 95 55 +135 87 51 +127 83 47 +119 79 43 +107 71 39 +95 67 35 +83 63 31 +75 55 27 +63 47 23 +51 43 19 +43 35 15 +255 235 223 +255 227 211 +255 219 199 +255 211 187 +255 207 179 +255 199 167 +255 191 155 +255 187 147 +255 179 131 +247 171 123 +239 163 115 +231 155 107 +223 147 99 +215 139 91 +207 131 83 +203 127 79 +255 238 220 +255 220 185 +255 203 151 +255 185 117 +255 168 85 +255 151 54 +255 134 25 +255 117 0 +243 109 0 +229 101 0 +216 93 0 +203 85 0 +190 77 0 +177 69 0 +164 61 0 +151 54 0 +255 255 239 +255 255 207 +255 255 175 +255 255 143 +255 255 111 +255 255 79 +255 255 47 +255 255 15 +255 255 0 +207 207 0 +175 175 0 +143 143 0 +111 111 0 +79 79 0 +47 47 0 +15 15 0 +255 255 115 +235 219 87 +215 187 67 +195 155 47 +175 123 31 +155 91 19 +135 67 7 +115 43 0 +255 223 223 +255 191 191 +255 159 159 +255 127 127 +255 95 95 +255 63 63 +255 31 31 +255 0 0 +239 0 0 +223 0 0 +207 0 0 +191 0 0 +175 0 0 +159 0 0 +143 0 0 +127 0 0 +111 0 0 +95 0 0 +79 0 0 +63 0 0 +47 0 0 +31 0 0 +15 0 0 +255 183 183 +243 163 163 +231 143 143 +219 123 123 +203 107 107 +191 91 91 +179 79 79 +167 63 63 +142 46 0 +134 39 0 +126 32 0 +117 25 0 +109 18 0 +101 11 0 +93 5 0 +85 0 0 +119 255 79 +112 240 75 +105 224 70 +97 208 65 +90 192 60 +82 176 55 +75 160 50 +67 144 45 +60 128 40 +53 112 35 +45 96 30 +38 80 25 +30 64 20 +23 48 15 +15 32 10 +7 15 4 +222 255 168 +199 228 148 +173 200 128 +149 173 107 +124 146 88 +100 119 68 +74 90 48 +50 63 29 +0 255 0 +0 223 0 +0 191 0 +0 159 0 +0 127 0 +0 95 0 +0 63 0 +0 31 0 +255 111 255 +255 0 255 +223 0 223 +191 0 191 +159 0 159 +127 0 127 +95 0 95 +63 0 63 +233 233 243 +196 196 225 +157 157 206 +119 119 187 +84 84 167 +65 65 131 +46 46 92 +27 27 52 +213 241 255 +191 235 255 +170 227 255 +149 221 255 +128 214 255 +106 207 255 +85 200 255 +63 191 255 +55 157 223 +47 143 191 +39 119 159 +31 95 127 +0 191 191 +0 127 127 +0 95 95 +0 63 63 +231 231 255 +198 198 255 +173 173 255 +140 140 255 +115 115 255 +82 82 255 +49 49 255 +24 24 255 +0 0 255 +0 0 231 +0 0 206 +0 0 181 +0 0 156 +0 0 132 +0 0 107 +0 0 82 +0 0 79 +0 0 63 +0 0 55 +0 0 39 +0 0 31 +0 0 15 +0 0 7 +0 255 255 +207 127 207 +183 111 183 +159 95 159 +135 79 135 +111 63 111 +87 47 87 +63 31 63 +39 15 39 diff --git a/data/tilesets/tomb.gif b/data/tilesets/tomb.gif new file mode 100644 index 0000000..86efffc Binary files /dev/null and b/data/tilesets/tomb.gif differ diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index bb36694..fec2434 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -4,9 +4,7 @@ #include #include -#include -#include "core/rendering/screen.hpp" #include "core/resources/resource_cache.hpp" #include "game/map/map.hpp" @@ -16,10 +14,6 @@ namespace { // es pot convertir en un membre modificable. constexpr float SPEED_PX_PER_S = 60.0F; - // Color de debug per a la caixa de col·lisió (ARGB opac). S'elimina - // quan el joc estiga afinat. - constexpr std::uint32_t DEBUG_COLLISION_ARGB = 0xFF00FFFFU; - } // namespace Player::Player(float x, float y, const Controls& controls) @@ -94,18 +88,6 @@ void Player::update(float delta_time) { setPosY(std::clamp(getPosY(), MIN_Y, MAX_Y)); } -void Player::render() { - // Debug: pinta la caixa de col·lisió (16×16) per sota del sprite perquè - // es vegen els límits del mapa de colisions alineats amb els passadissos. - Screen::get().fillRect( - static_cast(getPosX()) + COLLISION_X_OFFSET, - static_cast(getPosY()) + COLLISION_Y_OFFSET, - COLLISION_SIZE, - COLLISION_SIZE, - DEBUG_COLLISION_ARGB); - AnimatedSprite::render(); -} - auto Player::baseBox() const -> SDL_FRect { return { .x = getPosX() + static_cast(COLLISION_X_OFFSET), diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 1d9755c..af21ad9 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -37,7 +37,6 @@ class Player : public AnimatedSprite { Player(float x, float y, const Controls& controls); void update(float delta_time) override; - void render() override; // Y de la base del jugador (cara inferior de la caixa de colisió). Usada // per al Z-order contra els sarcs: peus per sobre d'un sarc → darrere; diff --git a/source/game/map/map.hpp b/source/game/map/map.hpp index 39b9179..d0e00fe 100644 --- a/source/game/map/map.hpp +++ b/source/game/map/map.hpp @@ -18,9 +18,9 @@ class Map { // Graella portada tal qual de l'AEE (4×4 sarcòfags, pitch 65×35). static constexpr int GRID_COLS = 4; static constexpr int GRID_ROWS = 5; - static constexpr int SARC_W = 50; + static constexpr int SARC_W = 48; static constexpr int SARC_H = 16; - static constexpr int PITCH_X = 66; + static constexpr int PITCH_X = 64; static constexpr int PITCH_Y = 32; // El pas entre sarcòfags (15×15) és exactament el que ha de cabre el @@ -39,7 +39,7 @@ class Map { // pas de 320×200 → 320×240 i reforçar la falsa perspectiva. static constexpr int HUD_H = 24; static constexpr int BACK_WALL_H = 24; - static constexpr int SIDE_WALL_W = 20; + static constexpr int SIDE_WALL_W = 24; // Origen del tauler dins del canvas (cantó superior-esquerra del primer // sarcòfag). Derivat: mur + passadís en cada eix. diff --git a/source/game/map/tilemap.cpp b/source/game/map/tilemap.cpp new file mode 100644 index 0000000..f6bf7b7 --- /dev/null +++ b/source/game/map/tilemap.cpp @@ -0,0 +1,138 @@ +#include "game/map/tilemap.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/rendering/screen.hpp" +#include "core/rendering/surface.hpp" +#include "core/resources/resource_cache.hpp" +#include "external/fkyaml_node.hpp" + +namespace { + + // Cada símbol del grid es mapeja a (col, fila) dins del tileset. + using TileCoord = std::pair; + using Legend = std::unordered_map; + + auto splitLines(const std::string& text) -> std::vector { + std::vector lines; + std::istringstream iss(text); + std::string line; + while (std::getline(iss, line)) { + if (!line.empty()) { lines.push_back(line); } + } + return lines; + } + + // La llegenda usa índexs lineals del tileset (esq→dre, dalt→baix). + // Els convertim a (col, fila) amb el nombre de tiles per fila del tileset. + auto parseLegend(const fkyaml::node& node, int tileset_cols) -> Legend { + Legend legend; + for (auto it = node.begin(); it != node.end(); ++it) { + const auto KEY_STR = it.key().get_value(); + if (KEY_STR.empty()) { continue; } + const auto INDEX = it.value().get_value(); + const int COL = INDEX % tileset_cols; + const int ROW = INDEX / tileset_cols; + legend[KEY_STR.front()] = {COL, ROW}; + } + return legend; + } + + // 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& 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 + +Tilemap::Tilemap(const std::string& yaml_path) { + std::ifstream file(yaml_path); + if (!file.is_open()) { + throw std::runtime_error("Tilemap: no s'ha pogut obrir " + yaml_path); + } + + auto yaml = fkyaml::node::deserialize(file); + + const auto TILESET_PATH = yaml["tileset"].get_value(); + const auto TILE_SIZE = yaml["tile_size"].get_value(); + if (yaml.contains("origin")) { + origin_x_ = yaml["origin"][0].get_value(); + origin_y_ = yaml["origin"][1].get_value(); + } + + // 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 la llegenda per saber quants tiles per fila té. + const auto& cache_name = TILESET_PATH; + Resource::Cache::get().loadSurface(cache_name, TILESET_PATH); + const auto TILESET = Resource::Cache::get().getSurface(cache_name); + const int TILESET_COLS = TILESET->getWidth() / TILE_SIZE; + + const auto LEGEND = parseLegend(yaml["legend"], TILESET_COLS); + const auto LINES = splitLines(yaml["data"].get_value()); + + const int ROWS = static_cast(LINES.size()); + const int COLS = ROWS > 0 ? static_cast(LINES.front().size()) : 0; + width_px_ = COLS * TILE_SIZE; + height_px_ = ROWS * TILE_SIZE; + pixels_.assign( + static_cast(width_px_) * static_cast(height_px_), + 0U); + + for (int row = 0; row < ROWS; ++row) { + const auto& line = LINES[row]; + for (int col = 0; col < COLS && col < static_cast(line.size()); ++col) { + const char SYMBOL = line[col]; + const auto IT = LEGEND.find(SYMBOL); + if (IT == LEGEND.end()) { continue; } + blitTile(*TILESET, TILE_SIZE, IT->second.first, IT->second.second, pixels_, width_px_, col * TILE_SIZE, row * TILE_SIZE); + } + } + + std::cout << "Tilemap: " << COLS << "x" << ROWS << " tiles (" + << width_px_ << "x" << height_px_ << " px) des de " << yaml_path << '\n'; +} + +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_); + + 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 & 0xFF000000U) != 0U) { dst_row[x] = PIXEL; } + } + } +} diff --git a/source/game/map/tilemap.hpp b/source/game/map/tilemap.hpp new file mode 100644 index 0000000..75b7aaf --- /dev/null +++ b/source/game/map/tilemap.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +// Tilemap basat en tiles 16×16 (o la mida que indique el yaml). El mapa es +// prerenderitza una sola vegada a un buffer ARGB propi a la construcció; a +// partir d'aquí, cada render és una simple còpia de píxels clipada sobre el +// canvas del joc. Això evita recórrer la graella de tiles cada frame. +// +// Les cel·les sense tile (p.ex. les caselles dels sarcòfags, que es faran +// actors independents) es deixen transparents (alpha=0) i el canvas +// subjacent hi queda visible. +class Tilemap { + public: + // Carrega el yaml (camí relatiu o absolut), el tileset referenciat i + // prerenderitza la graella a memòria. + explicit Tilemap(const std::string& yaml_path); + + // Dibuixa la textura prerenderitzada al canvas a l'origen del yaml. + // Offsets negatius permeten overscan (textura més gran que el canvas). + void render() const; + + [[nodiscard]] auto getWidth() const -> int { return width_px_; } + [[nodiscard]] auto getHeight() const -> int { return height_px_; } + [[nodiscard]] auto getOriginX() const -> int { return origin_x_; } + [[nodiscard]] auto getOriginY() const -> int { return origin_y_; } + + private: + int width_px_{0}; + int height_px_{0}; + int origin_x_{0}; + int origin_y_{0}; + std::vector pixels_; +}; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 8d8f990..a1a5f63 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -18,7 +18,8 @@ #include "utils/defaults.hpp" #include "utils/defines.hpp" -GameScene::GameScene() { +GameScene::GameScene() + : tilemap_("data/maps/pyramid.yaml") { Resource::Cache::get().loadSurface("player.gif", "data/player/player.gif"); Resource::Cache::get().loadAnimation("player.yaml", "data/player/player.yaml"); @@ -59,9 +60,11 @@ void GameScene::render() { const auto BG_ARGB = Resource::Cache::get().getPalette("ui")[UiColor::CANVAS_BG]; Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BG_ARGB); - // Fons del mapa (marcador, muros, sòl). Els sarcòfags es pinten després - // fila a fila entrellaçats amb els jugadors per al Z-order. - Map::renderBackground(); + // Mapa visual: textura prerenderitzada del tilemap. Els sarcòfags + // queden "foradats" (cel·les transparents) perquè passaran a ser + // actors independents; Map::collides encara els bloqueja per a les + // col·lisions del jugador. + tilemap_.render(); // Ordenem punters a jugadors per Y dels peus (ascendent = més al fons // primer). Usem un vector auxiliar per no tocar l'ordre d'entrada de @@ -73,24 +76,10 @@ void GameScene::render() { return a->getFeetY() < b->getFeetY(); }); - // Interleaving: per cada fila de sarcòfags, primer pintem els jugadors - // que hi queden "darrere" (peus per sobre de la cara inferior de la fila) - // i després la fila. Un jugador amb els peus davall d'una fila es pinta - // sobre d'ella (el seu cap tapa la fila superior si s'hi encavalca). - std::size_t p = 0; - for (int row = 0; row < Map::GRID_ROWS; ++row) { - const auto ROW_BOTTOM = static_cast(Map::getSarcRowBottomY(row)); - while (p < sorted.size() && sorted[p]->getFeetY() <= ROW_BOTTOM) { - sorted[p]->render(); - ++p; - } - Map::renderSarcsRow(row); - } - // Jugadors que queden per davall de l'última fila de sarcòfags. - while (p < sorted.size()) { - sorted[p]->render(); - ++p; - } + // De moment els sarcòfags no es pinten (seran actors independents); + // dibuixem tots els jugadors sobre el tilemap ja ordenats per Y. Quan + // els sarcs siguen sprites, tornarem a interleave per al Z-order. + for (auto* player : sorted) { player->render(); } Notifier::get().render(); RenderInfo::get().render(); diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 3dab755..5a3fbb2 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -4,6 +4,7 @@ #include #include "core/scene/scene.hpp" +#include "game/map/tilemap.hpp" class Player; @@ -16,6 +17,8 @@ class GameScene : public Scene { void render() override; private: + Tilemap tilemap_; + // Vector des del principi: AEE suportarà multijugador local; cada Player // té els seus propis Controls i entra al vector amb una crida extra. std::vector> players_; diff --git a/source/utils/defines.hpp b/source/utils/defines.hpp index c06c8a1..e4c3e7d 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.12.0"; // Single source of truth — Makefile el grep + constexpr const char* VERSION = "0.13.0"; // Single source of truth — Makefile el grep } // namespace Texts namespace GameCanvas {