From 7d0aca506a2a88f32688128ef1544d22bfe4612e Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Thu, 30 Apr 2026 19:16:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(decals):=20tiles=20de=20decal=20de=20s?= =?UTF-8?q?=C3=B2l=20compten=20com=20a=20pintats=20des=20del=20load?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/game/entities/concrete/hole.hpp | 1 - source/game/entities/concrete/mummy.cpp | 18 +++++-------- .../entities/entity_manager_loadlevel.cpp | 26 +++++++++++++++++++ source/game/map/map.cpp | 8 ++++++ source/game/map/map.hpp | 12 +++++++++ source/game/map/paint_layer.cpp | 21 +++++++++++++++ 6 files changed, 74 insertions(+), 12 deletions(-) diff --git a/source/game/entities/concrete/hole.hpp b/source/game/entities/concrete/hole.hpp index 1dbda64..13fcb25 100644 --- a/source/game/entities/concrete/hole.hpp +++ b/source/game/entities/concrete/hole.hpp @@ -37,5 +37,4 @@ class Hole : public StaticHazard { }; // TODO(hole-feature): afegir reset de `falling_through_hole_` i `hole_clip_bottom_y_` a `Player::disable()` per simetria. -// TODO(hole-feature): si apareix un quart callsite del càlcul "pixel→tile" o "tileRect", factoritzar a `Map::` (avui: Mummy::reactToCurrentTile, Mummy::isDecalAheadInDir, Player::integrateJump). // TODO(hole-feature): afegir tests dedicats del comportament de la mòmia amb forats (no requereixen Player real). diff --git a/source/game/entities/concrete/mummy.cpp b/source/game/entities/concrete/mummy.cpp index 40e08ab..90c0eca 100644 --- a/source/game/entities/concrete/mummy.cpp +++ b/source/game/entities/concrete/mummy.cpp @@ -291,13 +291,9 @@ void Mummy::reactToCurrentTile() { // per a una direcció negativa, el menor (`x`); per a zero, qualsevol // (no hi ha avenç en aquest eix). const auto BOX = getCollisionRect(); - const auto TS = static_cast(Map::tile_size); - const auto ORIG_X = static_cast(Map::tilemap_origin_x); - const auto ORIG_Y = static_cast(Map::tilemap_origin_y); const float LEAD_X = (dir_x_ > 0.0F) ? (BOX.x + BOX.w - 1.0F) : BOX.x; const float LEAD_Y = (dir_y_ > 0.0F) ? (BOX.y + BOX.h - 1.0F) : BOX.y; - const int CUR_COL = static_cast(std::floor((LEAD_X - ORIG_X) / TS)); - const int CUR_ROW = static_cast(std::floor((LEAD_Y - ORIG_Y) / TS)); + const auto [CUR_COL, CUR_ROW] = Map::pixelToTile(LEAD_X, LEAD_Y); // Sense canvi de tile no hi ha res a fer — això filtra també tots els // ticks intra-tile (la majoria, ~16 frames per tile a velocitats típiques). @@ -318,9 +314,10 @@ void Mummy::reactToCurrentTile() { // Si el tile nou conté un decal trigger (Hole o futur pinchos plans, // plaques, etc.), mitja volta immediata. `changeDirection` també // selecciona l'animació cardinal corresponent. + const auto TS = static_cast(Map::tile_size); const SDL_FRect TILE_RECT{ - .x = (static_cast(CUR_COL) * TS) + ORIG_X, - .y = (static_cast(CUR_ROW) * TS) + ORIG_Y, + .x = (static_cast(CUR_COL) * TS) + static_cast(Map::tilemap_origin_x), + .y = (static_cast(CUR_ROW) * TS) + static_cast(Map::tilemap_origin_y), .w = TS, .h = TS, }; @@ -444,8 +441,6 @@ auto Mummy::isDecalAheadInDir(float dx_signed, float dy_signed) const -> bool { // tile abans del veritablement adjacent al decal. const auto BOX = getCollisionRect(); const auto TS = static_cast(Map::tile_size); - const auto ORIG_X = static_cast(Map::tilemap_origin_x); - const auto ORIG_Y = static_cast(Map::tilemap_origin_y); const auto SIGN_OF = [](float v) -> float { if (v > 0.0F) { return 1.0F; } if (v < 0.0F) { return -1.0F; } @@ -453,8 +448,9 @@ auto Mummy::isDecalAheadInDir(float dx_signed, float dy_signed) const -> bool { }; const float CENTER_X = BOX.x + (BOX.w * 0.5F); const float CENTER_Y = BOX.y + (BOX.h * 0.5F); - const float TILE_X = (std::floor((CENTER_X - ORIG_X) / TS) * TS) + ORIG_X; - const float TILE_Y = (std::floor((CENTER_Y - ORIG_Y) / TS) * TS) + ORIG_Y; + const auto [CUR_COL, CUR_ROW] = Map::pixelToTile(CENTER_X, CENTER_Y); + const float TILE_X = (static_cast(CUR_COL) * TS) + static_cast(Map::tilemap_origin_x); + const float TILE_Y = (static_cast(CUR_ROW) * TS) + static_cast(Map::tilemap_origin_y); const SDL_FRect NEXT{ .x = TILE_X + (SIGN_OF(dx_signed) * TS), .y = TILE_Y + (SIGN_OF(dy_signed) * TS), diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index f776f17..0eb4181 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -420,6 +420,32 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) { if (OK) { ++loaded; } } + // Broadcast inicial: marca com a "pintats" tots els tiles ocupats per + // decals de sòl (Hole, i futurs pinchos plans, plaques, charcos...). + // Imprescindible perquè el jugador no pot trepitjar aquests tiles + // (cauria al forat) i els sarcòfags vecins no podrien completar el + // perímetre — la sala seria incompletable. Usa el canal semàntic + // `tile_painted` (lògica de joc), no `on_tile_entered` (presència + // física del jugador). Ho fem aquí, després del bucle de spawns, + // perquè tots els sarcòfags ja estan registrats al fan-out (la + // subscripció del manager és al ctor, anterior a qualsevol load). + int decal_broadcast = 0; + for (const auto& entity : entities_) { + if (!entity->isFloorDecal()) { continue; } + const auto BOX = entity->getCollisionRect(); + const auto [COL, ROW] = Map::pixelToTile(BOX.x, BOX.y); + bus_.publish(Event{ + .name = "tile_painted", + .payload = TilePos{.col = COL, .row = ROW}, + .player_id = -1, + }); + ++decal_broadcast; + } + if (decal_broadcast > 0) { + std::cout << "EntityManager: " << decal_broadcast + << " tile_painted broadcast inicial (decals de sòl)\n"; + } + std::cout << "EntityManager: " << loaded << " entitats carregades des de " << level_yaml_path << '\n'; } diff --git a/source/game/map/map.cpp b/source/game/map/map.cpp index b55912d..8271c0e 100644 --- a/source/game/map/map.cpp +++ b/source/game/map/map.cpp @@ -1,5 +1,6 @@ #include "game/map/map.hpp" +#include #include #include @@ -150,6 +151,13 @@ namespace Map { return true; } + auto pixelToTile(float px, float py) -> std::pair { + const auto TS = static_cast(tile_size); + const int COL = static_cast(std::floor((px - static_cast(tilemap_origin_x)) / TS)); + const int ROW = static_cast(std::floor((py - static_cast(tilemap_origin_y)) / TS)); + return {COL, ROW}; + } + auto makeInfo() -> Info { return { .playfield_x = playfield_x, diff --git a/source/game/map/map.hpp b/source/game/map/map.hpp index 62cf463..d9560c6 100644 --- a/source/game/map/map.hpp +++ b/source/game/map/map.hpp @@ -3,6 +3,7 @@ #include #include +#include // Mapa d'una piràmide: sala rectangular amb marcador superior, muros pels // quatre costats i graella de sarcòfags envoltada per un passadís a cada @@ -105,6 +106,17 @@ namespace Map { // usa el `PaintLayer` per filtrar quines cel·les accepten pintat. [[nodiscard]] auto isPaintable(int col, int row) -> bool; + // Converteix una coordenada píxel del canvas en (col, row) del + // tilemap. Assumeix `tile_size > 0`; si la coordenada cau fora del + // playfield, retorna el tile encara que sigui negatiu o estigui fora + // de rang — el caller valida si li cal. El càlcul és el típic + // `floor((p - origin) / tile_size)` que apareixia en diversos llocs; + // factoritzat aquí per coherència. Consumidors que tinguen un + // snapshot de `Map::Info` (p.ex. el Player, decoupling COUP-02) no + // han d'usar aquesta funció — han de seguir treballant amb el + // snapshot per no re-acoblar-se al namespace global. + [[nodiscard]] auto pixelToTile(float px, float py) -> std::pair; + // Snapshot plà de la geometria del mapa + punters a `collides` / // `insidePlayfield`. Pensat per ser passat per valor (POD petit, // trivialment copiable). Un cop construïda, llegeix els valors diff --git a/source/game/map/paint_layer.cpp b/source/game/map/paint_layer.cpp index 5d79b20..ced3edc 100644 --- a/source/game/map/paint_layer.cpp +++ b/source/game/map/paint_layer.cpp @@ -12,6 +12,27 @@ #include "game/map/map.hpp" #include "game/map/tilemap.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