feat(entities): caja-peus concèntrica i animacions direccionals a la momia, còpia de data/ sempre sincronitzada; v0.19.0

This commit is contained in:
JailDesigner committed 2026-04-21 23:21:32 +02:00
1 parent a3592d6c73
commit 32dcf264c4
9 files changed
+251 -58

No files matched your search

+109 -33
View File
@@ -1,5 +1,6 @@
#include "game/entities/concrete/mummy.hpp"
#include <algorithm>
#include <array>
#include <random>
#include <string>
@@ -20,9 +21,36 @@ namespace {
return k_rng_;
}
// Llegeix un bloc d'animació del YAML amb fallbacks tolerants (mateix
// patró que Door/Sarcophagus). Si la clau no apareix, retorna una
// animació d'1 frame loopant, neutra — la momia es podrà moure però
// visualment es quedarà estàtica.
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
std::vector<int> frames = {0};
float fps = 8.0F;
bool loop = true;
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
const auto& anim = yaml["animations"][key];
if (anim.contains("frames")) {
frames.clear();
for (const auto& f : anim["frames"]) {
// cppcheck-suppress useStlAlgorithm
frames.push_back(f.get_value<int>());
}
if (frames.empty()) { frames.push_back(0); }
}
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
}
return {frames, fps, loop};
}
} // namespace
Mummy::Mummy(const fkyaml::node& yaml) {
Mummy::Mummy(const fkyaml::node& yaml)
: anim_walk_down_(buildAnimation(yaml, "walk_down")),
anim_walk_up_(buildAnimation(yaml, "walk_up")),
anim_walk_lateral_(buildAnimation(yaml, "walk")) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
collides_ = true;
interactable_ = false;
@@ -43,16 +71,36 @@ Mummy::Mummy(const fkyaml::node& yaml) {
initial = yaml["ai"]["initial_direction"].get_value<std::string>();
}
if (initial == "right") {
setDirection(1.0F, 0.0F);
changeDirection(1.0F, 0.0F);
} else if (initial == "left") {
setDirection(-1.0F, 0.0F);
changeDirection(-1.0F, 0.0F);
} else if (initial == "up") {
setDirection(0.0F, -1.0F);
changeDirection(0.0F, -1.0F);
} else if (initial == "down") {
setDirection(0.0F, 1.0F);
changeDirection(0.0F, 1.0F);
}
}
void Mummy::changeDirection(float dx, float dy) {
setDirection(dx, dy);
// Prevalença lateral: si hi ha component horitzontal s'usa l'anim
// lateral + flip segons el signe. Només direccions purament verticals
// disparen walk_up/walk_down. Quan la direcció queda (0,0) (cul-de-sac
// amb NONE) no toquem l'animació: la momia es quedarà al frame actual
// fins que step() li torni a assignar direcció.
if (dx != 0.0F) {
anim_ = anim_walk_lateral_;
flip_ = (dx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
} else if (dy < 0.0F) {
anim_ = anim_walk_up_;
flip_ = SDL_FLIP_NONE;
} else if (dy > 0.0F) {
anim_ = anim_walk_down_;
flip_ = SDL_FLIP_NONE;
}
if (anim_.has_value()) { anim_->reset(); }
}
auto Mummy::chooseDirection(const std::vector<Direction>& allowed) -> Direction {
if (allowed.empty()) { return Direction::NONE; }
std::uniform_int_distribution<std::size_t> dist(0, allowed.size() - 1);
@@ -66,6 +114,31 @@ auto Mummy::candidateBox(float dx, float dy) const -> SDL_FRect {
return box;
}
void Mummy::snapToPlayfieldEdge(float dx, float dy) {
// La caja 16×16 té exactament l'amplada del corredor; si ens aturàvem
// a una posició subpíxel dins de l'última cel·la, qualsevol gir
// perpendicular col·lidiria amb el sarc adjacent al limit. Per això
// reduïm el dx/dy fins a la vora exacta del playfield en l'eix actiu
// (l'altre sempre és 0 perquè la momia només es mou en 1D per frame).
const auto BOX = getCollisionRect();
float snap_dx = 0.0F;
float snap_dy = 0.0F;
if (dx > 0.0F) {
snap_dx = std::max(0.0F, static_cast<float>(Map::playfield_x + Map::playfield_w) - BOX.x - BOX.w);
} else if (dx < 0.0F) {
snap_dx = std::min(0.0F, static_cast<float>(Map::playfield_x) - BOX.x);
}
if (dy > 0.0F) {
snap_dy = std::max(0.0F, static_cast<float>(Map::playfield_y + Map::playfield_h) - BOX.y - BOX.h);
} else if (dy < 0.0F) {
snap_dy = std::min(0.0F, static_cast<float>(Map::playfield_y) - BOX.y);
}
if (!Map::collides(candidateBox(snap_dx, snap_dy))) {
pixel_rect_.x += snap_dx;
pixel_rect_.y += snap_dy;
}
}
void Mummy::step(float delta_time) {
// Desplaçament nominal en aquest frame segons la direcció actual.
const float DX = dir_x_ * speed_ * delta_time;
@@ -84,60 +157,63 @@ void Mummy::step(float delta_time) {
return;
}
// Bloqueig: cal girar. Recollim les direccions perpendiculars a l'actual
// que tinguin un pròxim tram lliure. Evitem U-turn llevat que no hi hagi
// cap alternativa (comportament típic d'Oh Mummy!: no es recula sense
// motiu). Si la direcció perpendicular també està bloquejada, no s'hi
// inclou; si totes les perpendiculars estan bloquejades, afegim l'opcio
// oposada com a últim recurs (cul-de-sac).
const float STEP = speed_ * delta_time;
// Bloqueig per la vora del playfield: amarrem la caja a la vora exacta.
if (!Map::insidePlayfield(CANDIDATE)) { snapToPlayfieldEdge(DX, DY); }
// Cal girar. Preferim perpendiculars; només fem U-turn si no hi ha cap
// altra opció (comportament típic d'Oh Mummy!: no es recula sense motiu).
const auto POOLS = collectDirectionPools(speed_ * delta_time);
const auto& pool = !POOLS.perp.empty() ? POOLS.perp : POOLS.uturn;
applyChoice(chooseDirection(pool));
// No avancem aquest frame: la direcció nova s'aplicarà al pròxim step.
}
auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
struct Candidate {
float dx;
float dy;
Direction dir;
};
const std::array<Candidate, 4> ALL{{
{.dx = STEP, .dy = 0.0F, .dir = Direction::RIGHT},
{.dx = -STEP, .dy = 0.0F, .dir = Direction::LEFT},
{.dx = 0.0F, .dy = -STEP, .dir = Direction::UP},
{.dx = 0.0F, .dy = STEP, .dir = Direction::DOWN},
{.dx = step, .dy = 0.0F, .dir = Direction::RIGHT},
{.dx = -step, .dy = 0.0F, .dir = Direction::LEFT},
{.dx = 0.0F, .dy = -step, .dir = Direction::UP},
{.dx = 0.0F, .dy = step, .dir = Direction::DOWN},
}};
std::vector<Direction> perp_allowed;
std::vector<Direction> uturn_allowed;
DirectionPools pools;
for (const auto& c : ALL) {
const bool IS_CURRENT = (c.dx > 0.0F && dir_x_ > 0.0F) || (c.dx < 0.0F && dir_x_ < 0.0F) || (c.dy > 0.0F && dir_y_ > 0.0F) || (c.dy < 0.0F && dir_y_ < 0.0F);
const bool IS_UTURN = (c.dx > 0.0F && dir_x_ < 0.0F) || (c.dx < 0.0F && dir_x_ > 0.0F) || (c.dy > 0.0F && dir_y_ < 0.0F) || (c.dy < 0.0F && dir_y_ > 0.0F);
if (IS_CURRENT) { continue; } // ja hem vist que està bloquejada
const bool IS_CURRENT = (c.dx * dir_x_ > 0.0F) || (c.dy * dir_y_ > 0.0F);
if (IS_CURRENT) { continue; } // ja sabem que està bloquejada
const auto TEST = candidateBox(c.dx, c.dy);
if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; }
const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F);
if (IS_UTURN) {
uturn_allowed.push_back(c.dir);
pools.uturn.push_back(c.dir);
} else {
perp_allowed.push_back(c.dir);
pools.perp.push_back(c.dir);
}
}
return pools;
}
const auto& pool = !perp_allowed.empty() ? perp_allowed : uturn_allowed;
const auto CHOICE = chooseDirection(pool);
switch (CHOICE) {
void Mummy::applyChoice(Direction dir) {
switch (dir) {
case Direction::UP:
setDirection(0.0F, -1.0F);
changeDirection(0.0F, -1.0F);
break;
case Direction::DOWN:
setDirection(0.0F, 1.0F);
changeDirection(0.0F, 1.0F);
break;
case Direction::LEFT:
setDirection(-1.0F, 0.0F);
changeDirection(-1.0F, 0.0F);
break;
case Direction::RIGHT:
setDirection(1.0F, 0.0F);
changeDirection(1.0F, 0.0F);
break;
case Direction::NONE:
setDirection(0.0F, 0.0F);
changeDirection(0.0F, 0.0F);
break;
}
// No avancem aquest frame: la direcció nova s'aplicarà al pròxim step.
}
void Mummy::onPlayerContact(Player* /*player*/) {
+33
View File
@@ -4,6 +4,7 @@
#include <vector>
#include "external/fkyaml_node.hpp"
#include "game/entities/animation_component.hpp"
#include "game/entities/hazard.hpp"
// Mòmia: perill dinàmic que deambula pels passadís. A cada intersecció
@@ -44,4 +45,36 @@ class Mummy : public DynamicHazard {
private:
// Caixa de col·lisió candidata si l'entitat es desplaçara (dx, dy).
[[nodiscard]] auto candidateBox(float dx, float dy) const -> SDL_FRect;
// Amarra la caja a la vora exacta del playfield quan el candidat hi
// sortiria — sense això, amb la caja 16×16 iguala el corredor, la
// posició subpíxel dins de l'última cel·la impediria girar a cap
// perpendicular (col·lisionaria amb el sarc adjacent). Es crida abans
// de la lògica de gir.
void snapToPlayfieldEdge(float dx, float dy);
// Recull les direccions alternatives viables des de la posició actual:
// `perpendiculars` (girs) i `uturn` (inversa). La direcció actual s'exclou
// perquè ja sabem que està bloquejada. Cada candidata es valida amb
// `Map::insidePlayfield` + `Map::collides` sobre la caja resultant.
struct DirectionPools {
std::vector<Direction> perp;
std::vector<Direction> uturn;
};
[[nodiscard]] auto collectDirectionPools(float step) const -> DirectionPools;
// Canvi de direcció + aplicació de l'animació i flip corresponents.
// Tots els camins que trien nova direcció (ctor amb `initial_direction`,
// gir després d'un bloqueig) passen per ací per mantenir anim_ i
// flip_ en sincronia amb (dir_x_, dir_y_).
void changeDirection(float dx, float dy);
// Aplica la direcció triada (wrapper del switch en una sola crida).
void applyChoice(Direction dir);
// Animacions carregades del YAML, una per cada facing. Per a horitzontal
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta.
AnimationComponent anim_walk_down_;
AnimationComponent anim_walk_up_;
AnimationComponent anim_walk_lateral_;
};
+13
View File
@@ -128,6 +128,18 @@ class Entity {
// col·lisió coincideix amb el sprite.
void setCollisionBox(CollisionBox box) { collision_box_ = box; }
// Accés a la caixa crua (offsets + mida) — el loader la consulta per
// a col·locar el sprite quan `anchor: feet` ha d'alinear la caja (no
// el sprite) amb el tile. Nullopt si l'entitat no en té.
[[nodiscard]] auto getCollisionBox() const -> const std::optional<CollisionBox>& { return collision_box_; }
// Flip del sprite. Les entitats estàtiques mai no el toquen (queda a
// SDL_FLIP_NONE); les dinàmiques que miren cap als costats (p. ex.
// Mummy en direcció horitzontal) el canvien quan gira. El manager
// el passa a Surface::render.
[[nodiscard]] auto getFlip() const -> SDL_FlipMode { return flip_; }
void setFlip(SDL_FlipMode flip) { flip_ = flip; }
protected:
std::string name_;
TilePos tile_{};
@@ -137,5 +149,6 @@ class Entity {
std::shared_ptr<Surface> surface_{nullptr};
bool collides_{false};
bool interactable_{false};
SDL_FlipMode flip_{SDL_FLIP_NONE};
EventBus* bus_{nullptr};
};
@@ -51,7 +51,7 @@ void EntityManager::renderEntity(const Entity& e) {
.w = RECT.w,
.h = RECT.h,
};
surface->render(static_cast<int>(RECT.x), static_cast<int>(RECT.y), &SRC);
surface->render(static_cast<int>(RECT.x), static_cast<int>(RECT.y), &SRC, e.getFlip());
}
void EntityManager::renderAll() {
@@ -105,9 +105,16 @@ namespace {
return info;
}
// Bloc `collision: { size: [w, h], offset: [x, y] }` de la plantilla
// d'entitat. `offset` és opcional: si no apareix, s'entén "la caixa
// queda bottom-center del sprite" (convenció de falsa perspectiva).
// Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }`
// de la plantilla d'entitat.
// - Sense `base_size` ni `offset`: la caja queda alineada a la vora
// inferior del sprite i centrada horitzontalment (convenció antiga).
// - Amb `base_size: [bw, bh]`: la caja queda concèntrica amb el
// rectangle inferior bw×bh del sprite (els "peus" visuals), igual
// que fa el Player. Pot sobresortir per sobre/sota/costats del
// rectangle-base simètricament — així la caja representa el volum
// sòlid al voltant dels peus, no la silueta de tot el sprite.
// - `offset: [x, y]`: si apareix, sobreescriu el càlcul anterior.
// Retorna nullopt si no hi ha bloc, i l'entitat es queda sense caixa
// explícita (getCollisionRect torna tot el sprite).
auto readCollisionBox(const fkyaml::node& tmpl, float sprite_w, float sprite_h) -> std::optional<Entity::CollisionBox> {
@@ -120,6 +127,16 @@ namespace {
const int H = sz[1].get_value<int>();
int off_x = static_cast<int>((sprite_w - static_cast<float>(W)) / 2.0F);
int off_y = static_cast<int>(sprite_h - static_cast<float>(H));
if (c.contains("base_size")) {
const auto& bs = c["base_size"];
if (bs.is_sequence() && bs.size() == 2) {
const int BH = bs[1].get_value<int>();
// Centre vertical del rectangle-base (alineat a la vora
// inferior del sprite): sprite_h − bh/2. Caja centrada hi:
// off_y = sprite_h − bh/2 − H/2 = sprite_h − (bh + H)/2.
off_y = static_cast<int>(sprite_h) - ((BH + H) / 2);
}
}
if (c.contains("offset")) {
const auto& o = c["offset"];
if (o.is_sequence() && o.size() == 2) {
@@ -258,16 +275,26 @@ namespace {
const auto& p = spawn["position"];
const auto PX = tileToPixel(p[0].get_value<int>(), p[1].get_value<int>());
// Anchor "feet": el tile [col,row] és el tile dels peus. El sprite
// es centra horitzontalment sobre el tile i la seua vora inferior
// s'alinea amb la vora inferior del tile. Els píxels que sobresurten
// (alçada del sprite per damunt del tile dels peus) queden "per
// sobre" sense afectar cap col·lisió — falsa perspectiva.
// Anchor "feet": el tile [col,row] és el tile dels peus de l'actor.
// Si l'entitat té caixa de col·lisió explícita, la caixa s'alinea
// exactament amb el tile (el sprite queda col·locat per als offsets
// que la caja defineix — típicament sobresurt cap amunt per falsa
// perspectiva). Això és coherent amb el jugador, que també centra
// la seua 16×16 sobre el rectangle-base; la caja del tile i la del
// mapeig són la mateixa. Sense caixa, fallback històric: sprite
// centrat horitzontalment i amb la vora inferior a la vora inferior
// del tile.
float rect_x = PX.x;
float rect_y = PX.y;
if (anchor == "feet") {
rect_x = PX.x + (static_cast<float>(Map::tile_size) - sprite.width) / 2.0F;
rect_y = PX.y + static_cast<float>(Map::tile_size) - sprite.height;
const auto& box_opt = entity->getCollisionBox();
if (box_opt.has_value()) {
rect_x = PX.x - static_cast<float>(box_opt->offset_x);
rect_y = PX.y - static_cast<float>(box_opt->offset_y);
} else {
rect_x = PX.x + (static_cast<float>(Map::tile_size) - sprite.width) / 2.0F;
rect_y = PX.y + static_cast<float>(Map::tile_size) - sprite.height;
}
}
entity->setPixelRect({.x = rect_x, .y = rect_y, .w = sprite.width, .h = sprite.height});