#include "game/entities/concrete/door.hpp" #include #include "game/entities/entity_factory.hpp" #include "game/entities/event_bus.hpp" #include "game/entities/player.hpp" #include "game/map/map.hpp" Door::Door(const fkyaml::node& yaml) : anim_closed_(AnimationComponent::fromYaml(yaml, "closed")), anim_opening_(AnimationComponent::fromYaml(yaml, "opening")), anim_open_(AnimationComponent::fromYaml(yaml, "open")) { name_ = yaml.contains("name") ? yaml["name"].get_value() : 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) { // Només la porta OPEN despawna jugadors. CLOSED/OPENING ignoren el // contacte (i a més, en CLOSED, el `Map::collides` ja bloqueja el // moviment, així que el contacte no es dóna en condicions normals). if (state_ != State::OPEN || player == nullptr || player->isExited()) { return; } // Marquem el jugador com a EXITED i publiquem l'event d'agregació. // El check `isExited` de la guarda anterior garanteix que només // emetem l'event en la primera transició — `EntityManager` compta // emissions per decidir quan publicar `level_complete`. player->exit(); if (bus_ != nullptr) { bus_->publish(Event{ .name = "player_exited", .payload = std::monostate{}, .player_id = player->getId(), }); } } void Door::open() { if (state_ != State::CLOSED) { return; } state_ = State::OPENING; setAnimation(anim_opening_); // Marquem el passadís com a transitable: a partir d'aquest tic // `Map::collides` deixa de bloquejar el rect del passadís. La // transició d'animació OPENING → OPEN encara dura el seu temps, // però la geometria ja és permeable — coherent amb el feel de // "la porta s'obri i el jugador pot avançar al moment". Map::door_open = true; // 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::uniform({0}, 167.0F, false); } } REGISTER_ENTITY("door", Door)