feat(entities): afegeix stone i column com a muros decoratius
This commit is contained in:
1 parent
1c49248b72
commit
6749780356
18 files changed
+448
-36
No files matched your search
@@ -0,0 +1,27 @@
|
||||
#include <string>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/solid_decoration.hpp"
|
||||
|
||||
// Murs decoratius estàtics que comparteixen el mateix codi C++ però es
|
||||
// registren amb claus de factory diferents per a que el YAML els pugui
|
||||
// referenciar pel seu tipus i el editor els pugui distingir al picker.
|
||||
// La selecció del sub-sprite (frame del atlas) viatja al spawn YAML
|
||||
// (`frame: N`); l'aplica `placeGeneric` via `Entity::setAnimation`. Aquí no
|
||||
// cal cap lògica específica per a la variant.
|
||||
namespace {
|
||||
|
||||
class StaticSolid : public SolidDecoration {
|
||||
public:
|
||||
explicit StaticSolid(const fkyaml::node& yaml) {
|
||||
if (yaml.contains("name")) {
|
||||
name_ = yaml["name"].get_value<std::string>();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
REGISTER_ENTITY_AS("stone", StaticSolid, Stone)
|
||||
REGISTER_ENTITY_AS("column", StaticSolid, Column)
|
||||
@@ -125,6 +125,15 @@ class Entity {
|
||||
[[nodiscard]] auto collides() const -> bool { return collides_; }
|
||||
[[nodiscard]] auto interactable() const -> bool { return interactable_; }
|
||||
|
||||
// Marca d'entitat que ha de bloquejar el moviment del jugador (com un
|
||||
// mur). `collides_=true` només garanteix que l'entitat reb el callback
|
||||
// `onPlayerContact` quan el jugador la toca, no que el seu rect sigui
|
||||
// sòlid: dynamic hazards (mòmies, roques) tenen `collides_=true` però
|
||||
// no han de bloquejar el pas — el contacte resol el dany i la roca es
|
||||
// mou independentment. Per a "murs passius" (sarcs, columnes, debris)
|
||||
// posem aquest flag a true i `Map::collides` els tracta com a obstacle.
|
||||
[[nodiscard]] auto blocksMovement() const -> bool { return blocks_movement_; }
|
||||
|
||||
// Trigger d'àrea: l'entitat NO bloqueja el moviment (`collides_=false`)
|
||||
// però sí vol rebre `onPlayerContact` quan el jugador la solapa. Pensat
|
||||
// per a hazards "no físics" (forats al sòl, picos retràctils, plaques
|
||||
@@ -187,6 +196,14 @@ class Entity {
|
||||
// Entitats sense animació retornen `nullopt` → frame 0 per defecte.
|
||||
[[nodiscard]] auto getAnimation() const -> const std::optional<AnimationComponent>& { return anim_; }
|
||||
|
||||
// Sobreescriu l'`AnimationComponent` actual. L'usa el loader del nivell
|
||||
// per aplicar el camp opcional `frame: N` del spawn a entitats amb
|
||||
// variants estàtiques (debris, columnes, etc.): el spawn fixa una
|
||||
// animació de 1 frame al índex escollit i el render del manager clipa
|
||||
// el sub-sprite corresponent. També és útil per a tests que volen
|
||||
// controlar el frame mostrat sense passar pel YAML.
|
||||
void setAnimation(AnimationComponent anim) { anim_ = std::move(anim); }
|
||||
|
||||
// Alçada del cos de l'entitat en píxels. La consulten els filtres
|
||||
// de col·lisió per altura: si un jugador en l'aire passa per damunt
|
||||
// d'aquest valor (jump_z_ > height), no col·lisiona amb l'entitat —
|
||||
@@ -240,6 +257,7 @@ class Entity {
|
||||
bool interactable_{false};
|
||||
bool triggers_on_overlap_{false};
|
||||
bool is_floor_decal_{false};
|
||||
bool blocks_movement_{false};
|
||||
int height_{0};
|
||||
bool dead_{false};
|
||||
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "game/entities/concrete/door.hpp"
|
||||
#include "game/entities/concrete/sarcophagus.hpp"
|
||||
#include "game/entities/entity.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
|
||||
// NOTA: `loadFromLevel` viu a `entity_manager_loadlevel.cpp` per evitar
|
||||
// arrossegar `Resource::Cache`, `Surface` i les seues dependències (GIF,
|
||||
@@ -51,12 +52,23 @@ EntityManager::EntityManager() {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Connectar amb el check global de col·lisió del Map: les entitats
|
||||
// sòlides genèriques (debris, columnes, futurs murs) viuen al manager,
|
||||
// no a la graella fixa de sarcs, així que el `Map::collides` del Player
|
||||
// les ha de consultar via callback. Es desregistra al dtor.
|
||||
Map::setEntityCollider([this](const SDL_FRect& box) {
|
||||
return entityCollidesAt(box);
|
||||
});
|
||||
}
|
||||
|
||||
EntityManager::~EntityManager() {
|
||||
bus_.unsubscribe(sarc_sub_id_);
|
||||
bus_.unsubscribe(tile_sub_id_);
|
||||
bus_.unsubscribe(exit_sub_id_);
|
||||
// Desconnectar del Map: si quedem en namespace global apuntant a un
|
||||
// `this` que mor, qualsevol `Map::collides` posterior cauria en UB.
|
||||
Map::setEntityCollider(nullptr);
|
||||
}
|
||||
|
||||
void EntityManager::setExpectedPlayerCount(int n) {
|
||||
@@ -153,6 +165,17 @@ auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Ent
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto EntityManager::entityCollidesAt(const SDL_FRect& box) const -> bool {
|
||||
// Iteració lineal sobre entitats amb `blocksMovement()`. La convenció
|
||||
// d'overlap és la mateixa que `Map::collides` (estricta: el toc al
|
||||
// píxel no compta) per coherència en els passadissos del laberint.
|
||||
return std::ranges::any_of(entities_, [&box](const auto& e) {
|
||||
if (!e->blocksMovement()) { return false; }
|
||||
const auto R = e->getCollisionRect();
|
||||
return (R.x + R.w) > box.x && (box.x + box.w) > R.x && (R.y + R.h) > box.y && (box.y + box.h) > R.y;
|
||||
});
|
||||
}
|
||||
|
||||
auto EntityManager::getEntities() const -> std::vector<Entity*> {
|
||||
std::vector<Entity*> out;
|
||||
out.reserve(entities_.size());
|
||||
|
||||
@@ -181,6 +181,11 @@ class EntityManager {
|
||||
}
|
||||
|
||||
private:
|
||||
// Suport per al callback registrat a `Map::setEntityCollider`. Itera
|
||||
// les entitats amb `blocksMovement()` i fa overlap estricte (mateixa
|
||||
// convenció que `Map::collides`). El callback el registra el ctor.
|
||||
[[nodiscard]] auto entityCollidesAt(const SDL_FRect& box) const -> bool;
|
||||
|
||||
EventBus bus_;
|
||||
// Un sol contenidor lògic per a totes les entitats instanciades. La
|
||||
// discriminació de tipus la fan els virtuals (`onPlayerContact`,
|
||||
|
||||
@@ -395,6 +395,17 @@ namespace {
|
||||
}
|
||||
entity->setPixelRect({.x = rect_x, .y = rect_y, .w = sprite.width, .h = sprite.height});
|
||||
|
||||
// Variant del atlas: el spawn pot fixar un sub-sprite estàtic via
|
||||
// `frame: N`. El render del manager ja sap clipar (col, row) a
|
||||
// partir d'un índex lineal i la mida del frame; ací només cal
|
||||
// muntar un `AnimationComponent` d'un sol frame i imposar-lo a la
|
||||
// entitat. Cap entitat existent al joc emet aquest camp; només
|
||||
// els muros decoratius amb variants (stone, column, ...) l'usen.
|
||||
if (spawn.contains("frame")) {
|
||||
const int FRAME = spawn["frame"].get_value<int>();
|
||||
entity->setAnimation(AnimationComponent({FRAME}, 1.0F, false));
|
||||
}
|
||||
|
||||
manager.add(std::move(entity));
|
||||
return true;
|
||||
}
|
||||
@@ -459,31 +470,45 @@ void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
||||
if (addSpawn(spawn) != nullptr) { ++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).
|
||||
// Broadcast inicial: marca com a "pintats" tots els tiles que el
|
||||
// jugador no pot trepitjar mai, de manera que el perímetre dels
|
||||
// sarcòfags vecins ja els consideri visitats i la sala sigui
|
||||
// completable. Inclou dues famílies:
|
||||
// - Decals de sòl (Hole, i futurs pinchos plans, plaques, charcos):
|
||||
// trigger-on-overlap que mataria/atraparia al jugador en trepitjar.
|
||||
// - Entitats sòlides genèriques (debris, columnes...): obstacles
|
||||
// físics que `Map::collides` bloqueja, així `isPaintable` retorna
|
||||
// false per als seus tiles i ningú els pot pintar.
|
||||
// Usa el canal semàntic `tile_painted` (lògica de joc), no
|
||||
// `on_tile_entered` (presència física). Es fa 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;
|
||||
// Els decals de sòl (Hole, futurs pinchos plans...) són StaticHazards;
|
||||
// iterem tot `entities_` i el filtre per `isFloorDecal()` els tria.
|
||||
for (const auto& entity : entities_) {
|
||||
if (!entity->isFloorDecal()) { continue; }
|
||||
const bool IS_DECAL = entity->isFloorDecal();
|
||||
const bool IS_SOLID = entity->blocksMovement();
|
||||
if (!IS_DECAL && !IS_SOLID) { 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;
|
||||
// Per a entitats que ocupen més d'un tile (futurs murs 32×16,
|
||||
// 16×32, etc.), iterem tota la zona coberta pel rect. Per a decals
|
||||
// i blocs d'un sol tile cau a un sol broadcast (mateix comportament
|
||||
// que abans).
|
||||
const auto [COL0, ROW0] = Map::pixelToTile(BOX.x, BOX.y);
|
||||
const int COLS = (Map::tile_size > 0) ? std::max(1, static_cast<int>(BOX.w) / Map::tile_size) : 1;
|
||||
const int ROWS = (Map::tile_size > 0) ? std::max(1, static_cast<int>(BOX.h) / Map::tile_size) : 1;
|
||||
for (int dr = 0; dr < ROWS; ++dr) {
|
||||
for (int dc = 0; dc < COLS; ++dc) {
|
||||
bus_.publish(Event{
|
||||
.name = "tile_painted",
|
||||
.payload = TilePos{.col = COL0 + dc, .row = ROW0 + dr},
|
||||
.player_id = -1,
|
||||
});
|
||||
++decal_broadcast;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (decal_broadcast > 0) {
|
||||
Log::info("EntityManager", "{} tile_painted broadcast inicial (decals de sòl)", decal_broadcast);
|
||||
Log::info("EntityManager", "{} tile_painted broadcast inicial (decals + murs)", decal_broadcast);
|
||||
}
|
||||
|
||||
Log::info("EntityManager", "{} entitats + {} actors pending des de {}", loaded, pending_actors_.size(), level_yaml_path);
|
||||
|
||||
@@ -60,6 +60,9 @@ namespace EntityWriter {
|
||||
} else if (s.col.has_value() && s.row.has_value()) {
|
||||
out += " position: [" + std::to_string(*s.col) + ", " + std::to_string(*s.row) + "]\n";
|
||||
}
|
||||
if (s.frame.has_value()) {
|
||||
out += " frame: " + std::to_string(*s.frame) + "\n";
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -35,12 +35,16 @@ namespace EntityWriter {
|
||||
|
||||
// Cada spawn duplica l'estructura mínima del que el lector espera:
|
||||
// type sempre, i un dels (position, sarc_position) o cap (cas door).
|
||||
// `frame` és opcional i només l'emeten les entitats amb variants
|
||||
// estàtiques (debris, columnes); fixa quin sub-sprite del atlas
|
||||
// representa aquesta instància concreta.
|
||||
struct Spawn {
|
||||
std::string type;
|
||||
std::optional<int> col;
|
||||
std::optional<int> row;
|
||||
std::optional<int> sarc_col;
|
||||
std::optional<int> sarc_row;
|
||||
std::optional<int> frame;
|
||||
};
|
||||
|
||||
auto saveEntitiesBlock(const std::string& path, const std::vector<Spawn>& spawns) -> bool;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "game/entities/entity.hpp"
|
||||
|
||||
// Mur passiu: bloqueja moviment i salt però no té ni estat ni acció. Paral·lel
|
||||
// a `Decoration` (sense col·lisió) i `Interactable` (col·lisiona i té
|
||||
// estat/interact). Pensat per a debris/obstacles purament visuals que sí
|
||||
// han de detenir el jugador: pedres, columnes, restes.
|
||||
//
|
||||
// Diferència amb `Interactable`: no override `interact()` ni
|
||||
// `notifyTilePainted`. La caixa de col·lisió i alçada efectiva surten del
|
||||
// YAML (`collision.size`, `collision.height`); aquest ctor només fixa els
|
||||
// flags i un default raonable per a "mur infranquejable".
|
||||
class SolidDecoration : public Entity {
|
||||
public:
|
||||
using Entity::Entity;
|
||||
|
||||
SolidDecoration() {
|
||||
collides_ = true;
|
||||
interactable_ = false;
|
||||
height_ = 999;
|
||||
// Sí bloqueja el moviment del jugador: `Map::collides` el consulta
|
||||
// via callback per parar el moviment com si fos una paret sòlida.
|
||||
blocks_movement_ = true;
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user