feat(entities): andamiatge del sistema d'entitats (classes, factory, bus, YAML, placeholders, smoke tests)
This commit is contained in:
1 parent
984cc820c0
commit
96b71b0f65
48 files changed
+1957
No files matched your search
@@ -0,0 +1,85 @@
|
||||
#include "game/entities/concrete/door.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Carrega una animació d'un bloc concret (closed/opening/open) amb
|
||||
// fallbacks tolerants. Si la clau no existeix, retorna una animació
|
||||
// d'un sol frame loopant: no fa estat invàlid, simplement no hi ha res
|
||||
// per reproduir.
|
||||
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
||||
std::vector<int> frames = {0};
|
||||
float fps = 1.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
|
||||
|
||||
Door::Door(const fkyaml::node& yaml)
|
||||
: anim_closed_(buildAnimation(yaml, "closed")),
|
||||
anim_opening_(buildAnimation(yaml, "opening")),
|
||||
anim_open_(buildAnimation(yaml, "open")) {
|
||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("door");
|
||||
collides_ = true;
|
||||
interactable_ = true;
|
||||
anim_ = anim_closed_;
|
||||
}
|
||||
|
||||
void Door::setAnimation(const AnimationComponent& anim) {
|
||||
anim_ = anim;
|
||||
if (anim_.has_value()) { anim_->reset(); }
|
||||
}
|
||||
|
||||
void Door::update(float delta_time) {
|
||||
Entity::update(delta_time);
|
||||
|
||||
// Transició OPENING → OPEN: amb l'animació `opening` configurada com a
|
||||
// loop=false, `completed()` passa a true quan s'ha recorregut l'últim
|
||||
// frame. Per a animacions d'un sol frame, el component la considera
|
||||
// completada immediatament (1 frame = 0 passos), així que la transició
|
||||
// ocorre al pròxim tick.
|
||||
if (state_ == State::OPENING && anim_.has_value() && anim_->completed()) {
|
||||
state_ = State::OPEN;
|
||||
setAnimation(anim_open_);
|
||||
}
|
||||
}
|
||||
|
||||
void Door::onPlayerContact(Player* /*player*/) {
|
||||
if (state_ != State::OPEN || level_complete_emitted_) { return; }
|
||||
if (bus_ != nullptr) { bus_->publish("level_complete"); }
|
||||
level_complete_emitted_ = true;
|
||||
}
|
||||
|
||||
void Door::open() {
|
||||
if (state_ != State::CLOSED) { return; }
|
||||
state_ = State::OPENING;
|
||||
setAnimation(anim_opening_);
|
||||
// Edge case: si `opening` té ≤1 frame i és loop=true, mai es
|
||||
// completarà. Per força, forcem loop=false del frame únic aquí. Els
|
||||
// placeholders defineixen `loop: false` explícitament al YAML, així
|
||||
// que aquest fallback només actua si algú dissenya malament.
|
||||
if (anim_.has_value() && anim_->frameCount() <= 1 && anim_->loops()) {
|
||||
anim_ = AnimationComponent({0}, 6.0F, false);
|
||||
}
|
||||
}
|
||||
|
||||
REGISTER_ENTITY("door", Door)
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/animation_component.hpp"
|
||||
#include "game/entities/interactable.hpp"
|
||||
|
||||
// Porta de sortida del nivell. Tres estats:
|
||||
// CLOSED → inicial; bloqueja el pas.
|
||||
// OPENING → animació d'obertura en curs. Quan el component acaba →
|
||||
// transita a OPEN.
|
||||
// OPEN → accessible. Al contacte amb el jugador publica
|
||||
// `level_complete` al bus (si n'hi ha).
|
||||
//
|
||||
// El que decideix quan s'obri és extern (`EntityManager` comptant sarcs
|
||||
// oberts) via `open()`. L'animació `opening` es fa servir com a
|
||||
// temporitzador de transició (s'espera a `completed()`); si només té un
|
||||
// frame i és loop=false, es completa immediatament al següent tick.
|
||||
class Door : public Interactable {
|
||||
public:
|
||||
enum class State : std::uint8_t { CLOSED,
|
||||
OPENING,
|
||||
OPEN };
|
||||
|
||||
explicit Door(const fkyaml::node& yaml);
|
||||
|
||||
void update(float delta_time) override;
|
||||
void onPlayerContact(Player* player) override;
|
||||
|
||||
// Passa de CLOSED → OPENING. Idempotent: crides posteriors en estat
|
||||
// OPENING/OPEN són no-op.
|
||||
void open();
|
||||
|
||||
[[nodiscard]] auto getState() const -> State { return state_; }
|
||||
|
||||
private:
|
||||
void setAnimation(const AnimationComponent& anim);
|
||||
|
||||
State state_{State::CLOSED};
|
||||
bool level_complete_emitted_{false};
|
||||
|
||||
// Les 3 animacions precalculades al ctor des del YAML. L'activa
|
||||
// s'assigna a `anim_` (heretat d'Entity) segons l'estat.
|
||||
AnimationComponent anim_closed_;
|
||||
AnimationComponent anim_opening_;
|
||||
AnimationComponent anim_open_;
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "game/entities/concrete/floor_trap.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
|
||||
FloorTrap::FloorTrap(const fkyaml::node& yaml) {
|
||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("floor_trap");
|
||||
collides_ = false;
|
||||
interactable_ = false;
|
||||
|
||||
if (yaml.contains("cycle")) {
|
||||
const auto& c = yaml["cycle"];
|
||||
const float ACTIVE = c.contains("active_duration") ? c["active_duration"].get_value<float>() : 0.3F;
|
||||
const float INACTIVE = c.contains("inactive_duration") ? c["inactive_duration"].get_value<float>() : 2.0F;
|
||||
setCycle(ACTIVE, INACTIVE);
|
||||
}
|
||||
if (yaml.contains("stats") && yaml["stats"].contains("damage")) {
|
||||
setDamage(yaml["stats"]["damage"].get_value<int>());
|
||||
}
|
||||
}
|
||||
|
||||
void FloorTrap::onPlayerContact(Player* /*player*/) {
|
||||
if (!isActive()) { return; }
|
||||
if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); }
|
||||
}
|
||||
|
||||
REGISTER_ENTITY("floor_trap", FloorTrap)
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/hazard.hpp"
|
||||
|
||||
// Trampa de terra: equivalent a WallTrap però dissenyada per viure al
|
||||
// passadís (forats, picos del terra). El comportament és el mateix; la
|
||||
// distinció és purament semàntica/visual a efectes d'andamiatge.
|
||||
class FloorTrap : public StaticHazard {
|
||||
public:
|
||||
explicit FloorTrap(const fkyaml::node& yaml);
|
||||
|
||||
void onPlayerContact(Player* player) override;
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "game/entities/concrete/mummy.hpp"
|
||||
|
||||
#include <random>
|
||||
#include <string>
|
||||
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// RNG local al mòdul: compartit per totes les mòmies (no cal reproducció
|
||||
// determinista a l'andamiatge). Es reinicialitza amb una llavor fixa
|
||||
// per facilitar la depuració — si algun dia volem aleatoritat real,
|
||||
// llegir-la de std::random_device.
|
||||
auto rng() -> std::mt19937& {
|
||||
static std::mt19937 k_rng_{1337};
|
||||
return k_rng_;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Mummy::Mummy(const fkyaml::node& yaml) {
|
||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
|
||||
collides_ = true;
|
||||
interactable_ = false;
|
||||
|
||||
// Velocitat i dany des del bloc `stats` (si existeix).
|
||||
if (yaml.contains("stats")) {
|
||||
const auto& s = yaml["stats"];
|
||||
if (s.contains("speed")) { setSpeed(s["speed"].get_value<float>()); }
|
||||
if (s.contains("damage")) { setDamage(s["damage"].get_value<int>()); }
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return allowed[dist(rng())];
|
||||
}
|
||||
|
||||
void Mummy::onPlayerContact(Player* /*player*/) {
|
||||
if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); }
|
||||
}
|
||||
|
||||
REGISTER_ENTITY("mummy", Mummy)
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/hazard.hpp"
|
||||
|
||||
// Mòmia: perill dinàmic que deambula pels passadís. A cada intersecció
|
||||
// tria una direcció vàlida; l'algoritme per triar-la és un hook virtual
|
||||
// (`chooseDirection`) que la base implementa com a aleatori uniforme
|
||||
// sobre les direccions admissibles. Futurs derivats poden sesgar cap al
|
||||
// jugador sense tocar la lògica de moviment.
|
||||
//
|
||||
// Andamiatge: el concepte d'"intersecció" i la validació de direcció
|
||||
// requereixen conèixer el mapa. De moment l'exposem com un conjunt de
|
||||
// direccions admissibles que l'EntityManager injecta; la implementació
|
||||
// real arribarà quan cablegem l'entitat al game loop.
|
||||
class Mummy : public DynamicHazard {
|
||||
public:
|
||||
enum class Direction : std::uint8_t { NONE,
|
||||
UP,
|
||||
DOWN,
|
||||
LEFT,
|
||||
RIGHT };
|
||||
|
||||
explicit Mummy(const fkyaml::node& yaml);
|
||||
|
||||
// Hook d'IA. Donat un conjunt de direccions admissibles, decideix la
|
||||
// següent direcció a prendre. La implementació de la base tria una
|
||||
// aleatoria; si `allowed` està buit, retorna `NONE`.
|
||||
virtual auto chooseDirection(const std::vector<Direction>& allowed) -> Direction;
|
||||
|
||||
// Contacte amb el jugador: publica `player_hit` amb el dany.
|
||||
void onPlayerContact(Player* player) override;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "game/entities/concrete/rolling_rock.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Tradueix la direcció inicial textual del YAML a un vector 2D. Valors
|
||||
// no reconeguts → sense moviment (útil per spawn estàtics a tests).
|
||||
void applyInitialDirection(const std::string& dir, DynamicHazard& rock) {
|
||||
if (dir == "right") {
|
||||
rock.setDirection(1.0F, 0.0F);
|
||||
} else if (dir == "left") {
|
||||
rock.setDirection(-1.0F, 0.0F);
|
||||
} else if (dir == "up") {
|
||||
rock.setDirection(0.0F, -1.0F);
|
||||
} else if (dir == "down") {
|
||||
rock.setDirection(0.0F, 1.0F);
|
||||
} else {
|
||||
rock.setDirection(0.0F, 0.0F);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RollingRock::RollingRock(const fkyaml::node& yaml) {
|
||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("rolling_rock");
|
||||
collides_ = true;
|
||||
interactable_ = false;
|
||||
|
||||
if (yaml.contains("stats")) {
|
||||
const auto& s = yaml["stats"];
|
||||
if (s.contains("speed")) { setSpeed(s["speed"].get_value<float>()); }
|
||||
if (s.contains("damage")) { setDamage(s["damage"].get_value<int>()); }
|
||||
}
|
||||
std::string initial = "right";
|
||||
if (yaml.contains("ai") && yaml["ai"].contains("initial_direction")) {
|
||||
initial = yaml["ai"]["initial_direction"].get_value<std::string>();
|
||||
}
|
||||
applyInitialDirection(initial, *this);
|
||||
}
|
||||
|
||||
void RollingRock::onPlayerContact(Player* /*player*/) {
|
||||
if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); }
|
||||
}
|
||||
|
||||
REGISTER_ENTITY("rolling_rock", RollingRock)
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/hazard.hpp"
|
||||
|
||||
// Roca rodant: perill dinàmic amb trajectòria en línia recta. La direcció
|
||||
// inicial ve del bloc `ai:` del YAML (right/left/up/down) i es pot
|
||||
// sobreescriure per spawn. No canvia per ella mateixa; qui la vulgui
|
||||
// rebotar/aturar ha de cridar `setDirection(0,0)` externament.
|
||||
class RollingRock : public DynamicHazard {
|
||||
public:
|
||||
explicit RollingRock(const fkyaml::node& yaml);
|
||||
|
||||
void onPlayerContact(Player* player) override;
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "game/entities/concrete/sarcophagus.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
||||
std::vector<int> frames = {0};
|
||||
float fps = 1.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};
|
||||
}
|
||||
|
||||
// Llig `tile_size: [w, h]` del YAML. Fallback [1,1] si no existeix.
|
||||
void readTileSize(const fkyaml::node& yaml, int& w, int& h) {
|
||||
w = 1;
|
||||
h = 1;
|
||||
if (!yaml.contains("tile_size")) { return; }
|
||||
const auto& ts = yaml["tile_size"];
|
||||
if (ts.is_sequence() && ts.size() == 2) {
|
||||
w = ts[0].get_value<int>();
|
||||
h = ts[1].get_value<int>();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Sarcophagus::Sarcophagus(const fkyaml::node& yaml, int anchor_col, int anchor_row, const BlockedPredicate& is_blocked)
|
||||
: Sarcophagus(yaml) {
|
||||
place(anchor_col, anchor_row, is_blocked);
|
||||
}
|
||||
|
||||
Sarcophagus::Sarcophagus(const fkyaml::node& yaml)
|
||||
: anim_closed_(buildAnimation(yaml, "closed")),
|
||||
anim_opening_(buildAnimation(yaml, "opening")),
|
||||
anim_open_(buildAnimation(yaml, "open")) {
|
||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("sarcophagus");
|
||||
collides_ = true;
|
||||
interactable_ = true;
|
||||
readTileSize(yaml, tile_size_w_, tile_size_h_);
|
||||
anim_ = anim_closed_;
|
||||
}
|
||||
|
||||
void Sarcophagus::place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked) {
|
||||
anchor_col_ = anchor_col;
|
||||
anchor_row_ = anchor_row;
|
||||
tile_.col = anchor_col;
|
||||
tile_.row = anchor_row;
|
||||
computePerimeter(is_blocked);
|
||||
visited_.clear();
|
||||
}
|
||||
|
||||
void Sarcophagus::computePerimeter(const BlockedPredicate& is_blocked) {
|
||||
perimeter_tiles_.clear();
|
||||
|
||||
// El perímetre ortogonal d'un rectangle (w,h) de tiles són les files
|
||||
// de tiles immediatament N, S, E, O. No s'hi inclouen les cantonades
|
||||
// diagonals (tile NO, NE, SO, SE), perquè el jugador fa passes pels
|
||||
// quatre costats i no pot "tallar" per una diagonal: això garanteix
|
||||
// que el recorregut completa només si passa per cada costat.
|
||||
//
|
||||
// - Fila Nord: (col, row-1) per col ∈ [col0, col0+w-1]
|
||||
// - Fila Sud: (col, row+h) per col ∈ [col0, col0+w-1]
|
||||
// - Columna O: (col0-1, row) per row ∈ [row0, row0+h-1]
|
||||
// - Columna E: (col0+w, row) per row ∈ [row0, row0+h-1]
|
||||
|
||||
auto maybe_insert = [&](int col, int row) {
|
||||
if (is_blocked && is_blocked(col, row)) { return; }
|
||||
perimeter_tiles_.insert({col, row});
|
||||
};
|
||||
|
||||
// Nord i Sud
|
||||
for (int dc = 0; dc < tile_size_w_; ++dc) {
|
||||
maybe_insert(anchor_col_ + dc, anchor_row_ - 1);
|
||||
maybe_insert(anchor_col_ + dc, anchor_row_ + tile_size_h_);
|
||||
}
|
||||
// Oest i Est
|
||||
for (int dr = 0; dr < tile_size_h_; ++dr) {
|
||||
maybe_insert(anchor_col_ - 1, anchor_row_ + dr);
|
||||
maybe_insert(anchor_col_ + tile_size_w_, anchor_row_ + dr);
|
||||
}
|
||||
}
|
||||
|
||||
void Sarcophagus::update(float delta_time) {
|
||||
Entity::update(delta_time);
|
||||
|
||||
if (state_ == State::OPENING && anim_.has_value() && anim_->completed()) {
|
||||
state_ = State::OPEN;
|
||||
anim_ = anim_open_;
|
||||
if (anim_.has_value()) { anim_->reset(); }
|
||||
if (!opened_event_emitted_ && bus_ != nullptr) {
|
||||
bus_->publish("sarcophagus_opened", static_cast<Entity*>(this));
|
||||
opened_event_emitted_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Sarcophagus::notifyTileEntered(int col, int row) {
|
||||
if (state_ != State::CLOSED) { return; }
|
||||
const TileKey KEY{.col = col, .row = row};
|
||||
if (perimeter_tiles_.find(KEY) == perimeter_tiles_.end()) { return; }
|
||||
visited_.insert(KEY);
|
||||
if (visited_.size() >= perimeter_tiles_.size()) {
|
||||
state_ = State::OPENING;
|
||||
anim_ = anim_opening_;
|
||||
if (anim_.has_value()) { anim_->reset(); }
|
||||
}
|
||||
}
|
||||
|
||||
REGISTER_ENTITY("sarcophagus", Sarcophagus)
|
||||
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/animation_component.hpp"
|
||||
#include "game/entities/interactable.hpp"
|
||||
|
||||
// Sarcòfag: rectangle de tiles que el jugador ha de rodar completament pel
|
||||
// perímetre per "obrir". Tres estats (com la porta):
|
||||
// CLOSED → inicial; `visited_` s'acumula amb les notificacions.
|
||||
// OPENING → `visited_ == perimeter_tiles_`; reprodueix animació d'obertura.
|
||||
// OPEN → emitit l'event `sarcophagus_opened`.
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
// cauen dins d'un altre sarcòfag es descarten (predicat opcional). Si no
|
||||
// s'aporta el predicat, s'inclouen tots.
|
||||
class Sarcophagus : public Interactable {
|
||||
public:
|
||||
enum class State : std::uint8_t { CLOSED,
|
||||
OPENING,
|
||||
OPEN };
|
||||
|
||||
// `is_blocked` rep un (col,row) i retorna true si el tile està ocupat
|
||||
// per un altre sarcòfag o és fora del playfield (és a dir, NO és un
|
||||
// tile de corredor vàlid). L'usa `EntityManager` passant-li
|
||||
// `Map::collides` traduït a tiles. Si és nullptr, cap tile es descarta
|
||||
// i el perímetre es computa purament geomètric (útil als tests).
|
||||
using BlockedPredicate = std::function<bool(int col, int row)>;
|
||||
|
||||
Sarcophagus(const fkyaml::node& yaml, int anchor_col, int anchor_row, const BlockedPredicate& is_blocked);
|
||||
explicit Sarcophagus(const fkyaml::node& yaml); // variant sense ancoratge (usat pel factory)
|
||||
|
||||
// Configura ancoratge i (re)calcula perímetre. Útil quan el factory
|
||||
// crea l'instància abans de conèixer la posició; el `EntityLoader`
|
||||
// invoca aquesta funció amb les coordenades del spawn.
|
||||
void place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked);
|
||||
|
||||
void update(float delta_time) override;
|
||||
void notifyTileEntered(int col, int row) override;
|
||||
|
||||
[[nodiscard]] auto getState() const -> State { return state_; }
|
||||
[[nodiscard]] auto getPerimeterSize() const -> std::size_t { return perimeter_tiles_.size(); }
|
||||
[[nodiscard]] auto getVisitedCount() const -> std::size_t { return visited_.size(); }
|
||||
|
||||
// Dimensions del rectangle en tiles (p.ex. 3×1 per al sarcòfag
|
||||
// estàndard). Exposat per al smoke test i per al render futur.
|
||||
[[nodiscard]] auto getTileWidth() const -> int { return tile_size_w_; }
|
||||
[[nodiscard]] auto getTileHeight() const -> int { return tile_size_h_; }
|
||||
|
||||
private:
|
||||
struct TileKey {
|
||||
int col;
|
||||
int row;
|
||||
auto operator==(const TileKey& o) const -> bool { return col == o.col && row == o.row; }
|
||||
};
|
||||
struct TileKeyHash {
|
||||
auto operator()(const TileKey& k) const -> std::size_t {
|
||||
// Combinació simple; els valors són petits (<100) i el set no és gran.
|
||||
return (static_cast<std::size_t>(k.col) * 131U) ^ static_cast<std::size_t>(k.row);
|
||||
}
|
||||
};
|
||||
|
||||
void computePerimeter(const BlockedPredicate& is_blocked);
|
||||
|
||||
int tile_size_w_{1};
|
||||
int tile_size_h_{1};
|
||||
int anchor_col_{0};
|
||||
int anchor_row_{0};
|
||||
|
||||
std::unordered_set<TileKey, TileKeyHash> perimeter_tiles_;
|
||||
std::unordered_set<TileKey, TileKeyHash> visited_;
|
||||
|
||||
State state_{State::CLOSED};
|
||||
|
||||
AnimationComponent anim_closed_;
|
||||
AnimationComponent anim_opening_;
|
||||
AnimationComponent anim_open_;
|
||||
|
||||
bool opened_event_emitted_{false};
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "game/entities/concrete/torch.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Ajut: extreu l'animació `idle` del bloc `animations` del YAML si existeix,
|
||||
// amb valors per defecte que sempre produïxen una animació vàlida (1 frame).
|
||||
auto buildIdleAnimation(const fkyaml::node& yaml) -> AnimationComponent {
|
||||
std::vector<int> frames = {0};
|
||||
float fps = 1.0F;
|
||||
bool loop = true;
|
||||
if (yaml.contains("animations") && yaml["animations"].contains("idle")) {
|
||||
const auto& anim = yaml["animations"]["idle"];
|
||||
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
|
||||
|
||||
Torch::Torch(const fkyaml::node& yaml) {
|
||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("torch");
|
||||
collides_ = false;
|
||||
interactable_ = false;
|
||||
anim_ = buildIdleAnimation(yaml);
|
||||
}
|
||||
|
||||
REGISTER_ENTITY("torch", Torch)
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/decoration.hpp"
|
||||
|
||||
// Antorxa decorativa: només anima. Sense col·lisió, sense interacció.
|
||||
class Torch : public Decoration {
|
||||
public:
|
||||
explicit Torch(const fkyaml::node& yaml);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "game/entities/concrete/wall_trap.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
|
||||
WallTrap::WallTrap(const fkyaml::node& yaml) {
|
||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("wall_trap");
|
||||
collides_ = false;
|
||||
interactable_ = false;
|
||||
|
||||
if (yaml.contains("cycle")) {
|
||||
const auto& c = yaml["cycle"];
|
||||
const float ACTIVE = c.contains("active_duration") ? c["active_duration"].get_value<float>() : 0.5F;
|
||||
const float INACTIVE = c.contains("inactive_duration") ? c["inactive_duration"].get_value<float>() : 1.5F;
|
||||
setCycle(ACTIVE, INACTIVE);
|
||||
}
|
||||
if (yaml.contains("stats") && yaml["stats"].contains("damage")) {
|
||||
setDamage(yaml["stats"]["damage"].get_value<int>());
|
||||
}
|
||||
}
|
||||
|
||||
void WallTrap::onPlayerContact(Player* /*player*/) {
|
||||
if (!isActive()) { return; }
|
||||
if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); }
|
||||
}
|
||||
|
||||
REGISTER_ENTITY("wall_trap", WallTrap)
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/hazard.hpp"
|
||||
|
||||
// Trampa de paret: perill estàtic amb cicle actiu/inactiu (p.ex. punxes
|
||||
// que surten periòdicament). Contacte → dany només quan `isActive()`.
|
||||
class WallTrap : public StaticHazard {
|
||||
public:
|
||||
explicit WallTrap(const fkyaml::node& yaml);
|
||||
|
||||
void onPlayerContact(Player* player) override;
|
||||
};
|
||||
Reference in new issue
Block a user