fix(sarcophagus): inclou les cantonades diagonals al perímetre

This commit is contained in:
JailDesigner committed 2026-05-12 09:00:56 +02:00
1 parent d26abcb9d5
commit 642e0653b2
3 files changed
+43 -30

No files matched your search

+12 -10
View File
@@ -97,14 +97,16 @@ void Sarcophagus::place(int anchor_col, int anchor_row, const BlockedPredicate&
void Sarcophagus::computePerimeter(const BlockedPredicate& is_blocked) { void Sarcophagus::computePerimeter(const BlockedPredicate& is_blocked) {
perimeter_tiles_.clear(); perimeter_tiles_.clear();
// El perímetre ortogonal d'un rectangle (w,h) de tiles són les files // El perímetre d'un rectangle (w,h) de tiles són les files de tiles
// de tiles immediatament N, S, E, O. No s'hi inclouen les cantonades // immediatament adjacents: N, S, E, O *més* les quatre cantonades
// diagonals (tile NO, NE, SO, SE), perquè el jugador fa passes pels // diagonals (NO, NE, SO, SE). En la pràctica el jugador trepitja
// quatre costats i no pot "tallar" per una diagonal: això garanteix // aquestes cantonades en doblar les cantonades del sarcòfag, així que
// que el recorregut completa només si passa per cada costat. // si no s'hi compten, el cerco no es pot tancar visualment encara
// que es completi a nivell de codi (o, pitjor, s'obre sense que el
// jugador hagi clos realment el contorn).
// //
// - Fila Nord: (col, row-1) per col ∈ [col0, col0+w-1] // - Fila Nord: (col, row-1) per col ∈ [col0-1, col0+w] (inclou NO i NE)
// - Fila Sud: (col, row+h) per col ∈ [col0, col0+w-1] // - Fila Sud: (col, row+h) per col ∈ [col0-1, col0+w] (inclou SO i SE)
// - Columna O: (col0-1, row) per row ∈ [row0, row0+h-1] // - Columna O: (col0-1, row) per row ∈ [row0, row0+h-1]
// - Columna E: (col0+w, row) per row ∈ [row0, row0+h-1] // - Columna E: (col0+w, row) per row ∈ [row0, row0+h-1]
@@ -113,12 +115,12 @@ void Sarcophagus::computePerimeter(const BlockedPredicate& is_blocked) {
perimeter_tiles_.insert({col, row}); perimeter_tiles_.insert({col, row});
}; };
// Nord i Sud // Nord i Sud (incloent les quatre cantonades diagonals)
for (int dc = 0; dc < tile_size_w_; ++dc) { for (int dc = -1; dc <= tile_size_w_; ++dc) {
maybe_insert(anchor_col_ + dc, anchor_row_ - 1); maybe_insert(anchor_col_ + dc, anchor_row_ - 1);
maybe_insert(anchor_col_ + dc, anchor_row_ + tile_size_h_); maybe_insert(anchor_col_ + dc, anchor_row_ + tile_size_h_);
} }
// Oest i Est // Oest i Est (les cantonades ja les ha posat el bucle N/S)
for (int dr = 0; dr < tile_size_h_; ++dr) { for (int dr = 0; dr < tile_size_h_; ++dr) {
maybe_insert(anchor_col_ - 1, anchor_row_ + dr); maybe_insert(anchor_col_ - 1, anchor_row_ + dr);
maybe_insert(anchor_col_ + tile_size_w_, anchor_row_ + dr); maybe_insert(anchor_col_ + tile_size_w_, anchor_row_ + dr);
@@ -18,7 +18,10 @@
// El càlcul de `perimeter_tiles_` es fa al ctor a partir de la posició del // El càlcul de `perimeter_tiles_` es fa al ctor a partir de la posició del
// sarcòfag a la graella del Map (`anchor_col`, `anchor_row`) i de les // sarcòfag a la graella del Map (`anchor_col`, `anchor_row`) i de les
// dimensions en tiles (`tile_size_w_`, `tile_size_h_`, llegits del YAML // dimensions en tiles (`tile_size_w_`, `tile_size_h_`, llegits del YAML
// plantilla). Es recorren els tiles adjacents per N/S/E/O; els que // plantilla). S'inclouen els tiles adjacents per N/S/E/O i també les
// quatre cantonades diagonals (NO/NE/SO/SE), perquè el jugador les
// trepitja en doblar les cantonades del sarcòfag i, sense comptar-les,
// el cerco s'obriria abans d'haver-se tancat visualment. Els tiles que
// cauen dins d'un altre sarcòfag es descarten (predicat opcional). Si no // cauen dins d'un altre sarcòfag es descarten (predicat opcional). Si no
// s'aporta el predicat, s'inclouen tots. // s'aporta el predicat, s'inclouen tots.
class Sarcophagus : public Interactable { class Sarcophagus : public Interactable {
+27 -19
View File
@@ -14,7 +14,8 @@
// T7 plantilles — carrega els 7 YAML d'entitat sense excepcions. // T7 plantilles — carrega els 7 YAML d'entitat sense excepcions.
// T8 decoration — Torch no col·lisiona ni és interactable. // T8 decoration — Torch no col·lisiona ni és interactable.
// T10 torch anim — Torch llegeix 4 frames del YAML i el tick avança frame. // T10 torch anim — Torch llegeix 4 frames del YAML i el tick avança frame.
// T11 sarcofag — tile_size [3,1] llegit; perímetre 8; falsa perspectiva: // T11 sarcofag — tile_size [3,1] llegit; perímetre 12 (incloent les
// quatre cantonades diagonals); falsa perspectiva:
// el sprite.height no canvia la caixa lògica 48×16. // el sprite.height no canvia la caixa lògica 48×16.
// T12 collision — `Entity::CollisionBox` encastada al sprite: la caixa // T12 collision — `Entity::CollisionBox` encastada al sprite: la caixa
// retornada coincideix amb les dimensions del bloc // retornada coincideix amb les dimensions del bloc
@@ -143,15 +144,15 @@ namespace {
fkyaml::node tmpl = EntityLoader::loadTemplate("sarcophagus"); fkyaml::node tmpl = EntityLoader::loadTemplate("sarcophagus");
auto sarc = std::make_unique<Sarcophagus>(tmpl); auto sarc = std::make_unique<Sarcophagus>(tmpl);
// Ancorat a (col=1, row=1) amb tile_size=[3,1] → rectangle de tiles // Ancorat a (col=1, row=1) amb tile_size=[3,1] → rectangle de tiles
// cobreix (1,1), (2,1), (3,1). Perímetre ortogonal esperat (sense // cobreix (1,1), (2,1), (3,1). Perímetre esperat (sense
// BlockedPredicate): // BlockedPredicate), incloent les quatre cantonades diagonals:
// Nord: (1,0), (2,0), (3,0) // Nord (amb NO i NE): (0,0), (1,0), (2,0), (3,0), (4,0)
// Sud: (1,2), (2,2), (3,2) // Sud (amb SO i SE): (0,2), (1,2), (2,2), (3,2), (4,2)
// Oest: (0,1) // Oest: (0,1)
// Est: (4,1) // Est: (4,1)
// Total: 8 tiles. // Total: 12 tiles.
sarc->place(1, 1, {}); sarc->place(1, 1, {});
REQUIRE(sarc->getPerimeterSize() == 8); REQUIRE(sarc->getPerimeterSize() == 12);
REQUIRE(sarc->getState() == Sarcophagus::State::CLOSED); REQUIRE(sarc->getState() == Sarcophagus::State::CLOSED);
// Visitem un tile que NO és al perímetre: res canvia. // Visitem un tile que NO és al perímetre: res canvia.
@@ -160,10 +161,12 @@ namespace {
// Visitem tots els del perímetre. // Visitem tots els del perímetre.
const std::vector<std::pair<int, int>> PERIM = { const std::vector<std::pair<int, int>> PERIM = {
{1, 0}, {2, 0}, {3, 0}, {1, 2}, {2, 2}, {3, 2}, {0, 1}, {4, 1}, {0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 0},
{0, 2}, {1, 2}, {2, 2}, {3, 2}, {4, 2},
{0, 1}, {4, 1},
}; };
for (const auto& [c, r] : PERIM) { sarc->notifyTilePainted(c, r); } for (const auto& [c, r] : PERIM) { sarc->notifyTilePainted(c, r); }
REQUIRE(sarc->getVisitedCount() == 8); REQUIRE(sarc->getVisitedCount() == 12);
REQUIRE(sarc->getState() == Sarcophagus::State::OPENING); REQUIRE(sarc->getState() == Sarcophagus::State::OPENING);
// Tickem prou per consumir l'animació `opening` (4 frames @ 6fps = // Tickem prou per consumir l'animació `opening` (4 frames @ 6fps =
@@ -192,13 +195,14 @@ namespace {
manager.add(std::move(door)); manager.add(std::move(door));
manager.setDoor(door_ptr); manager.setDoor(door_ptr);
// Forcem l'obertura dels dos sarcs visitant els seus perímetres. // Forcem l'obertura dels dos sarcs visitant els seus perímetres
// (incloent les cantonades diagonals NO/NE/SO/SE).
auto forceOpen = [](Sarcophagus* s) { auto forceOpen = [](Sarcophagus* s) {
const int COL0 = s->getTilePos().col; const int COL0 = s->getTilePos().col;
const int ROW0 = s->getTilePos().row; const int ROW0 = s->getTilePos().row;
const int W = s->getTileWidth(); const int W = s->getTileWidth();
const int H = s->getTileHeight(); const int H = s->getTileHeight();
for (int dc = 0; dc < W; ++dc) { for (int dc = -1; dc <= W; ++dc) {
s->notifyTilePainted(COL0 + dc, ROW0 - 1); s->notifyTilePainted(COL0 + dc, ROW0 - 1);
s->notifyTilePainted(COL0 + dc, ROW0 + H); s->notifyTilePainted(COL0 + dc, ROW0 + H);
} }
@@ -336,11 +340,14 @@ namespace {
auto sarc = std::make_unique<Sarcophagus>(EntityLoader::loadTemplate("sarcophagus")); auto sarc = std::make_unique<Sarcophagus>(EntityLoader::loadTemplate("sarcophagus"));
sarc->place(3, 4, {}); sarc->place(3, 4, {});
Sarcophagus* sarc_raw = manager.addSarcophagus(std::move(sarc)); Sarcophagus* sarc_raw = manager.addSarcophagus(std::move(sarc));
REQUIRE(sarc_raw->getPerimeterSize() == 8); REQUIRE(sarc_raw->getPerimeterSize() == 12);
// Les 8 tiles del perímetre: N (3..5,3), S (3..5,5), O (2,4), E (6,4). // Les 12 tiles del perímetre, incloent les cantonades diagonals:
// N (amb NO/NE): (2..6, 3)
// S (amb SO/SE): (2..6, 5)
// O: (2,4) E: (6,4)
const std::pair<int, int> PERIM[] = { const std::pair<int, int> PERIM[] = {
{3, 3}, {4, 3}, {5, 3}, {2, 3}, {3, 3}, {4, 3}, {5, 3}, {6, 3},
{3, 5}, {4, 5}, {5, 5}, {2, 5}, {3, 5}, {4, 5}, {5, 5}, {6, 5},
{2, 4}, {6, 4}, {2, 4}, {6, 4},
}; };
for (const auto& [c, r] : PERIM) { for (const auto& [c, r] : PERIM) {
@@ -361,12 +368,13 @@ namespace {
REQUIRE(tmpl["sprite"]["size"][0].get_value<int>() == 48); REQUIRE(tmpl["sprite"]["size"][0].get_value<int>() == 48);
REQUIRE(tmpl["sprite"]["size"][1].get_value<int>() == 32); REQUIRE(tmpl["sprite"]["size"][1].get_value<int>() == 32);
// Amb tile_size [3,1], un sarc a ancoratge (3,4) conserva els 8 // Amb tile_size [3,1], un sarc a ancoratge (3,4) té 12 tiles de
// tiles de perímetre (no es converteix en 10 per culpa del [3,2] // perímetre: 5 al nord (amb NO/NE), 5 al sud (amb SO/SE), 1 a
// antic). Això blinda la geometria contra regressions al YAML. // l'oest i 1 a l'est. Això blinda la geometria contra regressions
// al YAML (p. ex. un retorn accidental al tile_size [3,2] antic).
auto sarc = std::make_unique<Sarcophagus>(tmpl); auto sarc = std::make_unique<Sarcophagus>(tmpl);
sarc->place(3, 4, {}); sarc->place(3, 4, {});
REQUIRE(sarc->getPerimeterSize() == 8); REQUIRE(sarc->getPerimeterSize() == 12);
} }
void testCollisionBox() { void testCollisionBox() {