diff --git a/CMakeLists.txt b/CMakeLists.txt index 49bcd8b..a6a777c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,7 @@ set(ENTITY_SOURCES source/game/entities/concrete/rolling_rock.cpp source/game/entities/concrete/wall_trap.cpp source/game/entities/concrete/floor_trap.cpp + source/game/entities/concrete/dust.cpp ) set(APP_SOURCES diff --git a/data/entities/dust/dust.gif b/data/entities/dust/dust.gif new file mode 100644 index 0000000..7505880 Binary files /dev/null and b/data/entities/dust/dust.gif differ diff --git a/data/entities/dust/dust.yaml b/data/entities/dust/dust.yaml new file mode 100644 index 0000000..7b32845 --- /dev/null +++ b/data/entities/dust/dust.yaml @@ -0,0 +1,22 @@ +name: dust +type: effect +sprite: + path: dust.gif + size: [40, 16] + +# El polvet és purament visual: no col·lisiona, no interactua, no fa +# dany. Només viu mentre dura l'animació triada (`jump` o `land`) i +# desapareix per `markDead` quan acaba. +flags: + collides: false + interactable: false + +# Dues animacions, no-loop: una per a cada moment del salt. El spawner +# (GameScene, en resposta a events del bus) instancia la variant +# apropiada via `setVariant` abans del primer tick. +# jump: frames 0..6 (7 frames) → reproduït al despegue. +# land: frames 7..12 (6 frames) → reproduït a l'aterratge. +# fps: 10 = 100 ms per frame. +animations: + jump: { frames: [0, 1, 2, 3, 4, 5, 6], fps: 10, loop: false } + land: { frames: [7, 8, 9, 10, 11, 12], fps: 10, loop: false } diff --git a/source/game/entities/concrete/dust.cpp b/source/game/entities/concrete/dust.cpp new file mode 100644 index 0000000..29aedc5 --- /dev/null +++ b/source/game/entities/concrete/dust.cpp @@ -0,0 +1,57 @@ +#include "game/entities/concrete/dust.hpp" + +#include +#include + +#include "game/entities/entity_factory.hpp" + +namespace { + + // Llig una animació opcional del bloc `animations` del YAML, amb + // valors per defecte segurs (1 frame, fps=1, sense loop). Si la + // clau no existeix, retornem una animació "ja completada" — així, + // un Dust spawnejat amb una variant inexistent al YAML mor al + // pròxim frame sense petar. + auto buildAnim(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent { + std::vector frames = {0}; + float fps = 1.0F; + bool loop = false; + 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()); + } + if (frames.empty()) { frames.push_back(0); } + } + if (anim.contains("fps")) { fps = anim["fps"].get_value(); } + if (anim.contains("loop")) { loop = anim["loop"].get_value(); } + } + return {frames, fps, loop}; + } + +} // namespace + +Dust::Dust(const fkyaml::node& yaml) + : anim_jump_(buildAnim(yaml, "jump")), + anim_land_(buildAnim(yaml, "land")) { + name_ = yaml.contains("name") ? yaml["name"].get_value() : std::string("dust"); + collides_ = false; + interactable_ = false; + // Per defecte JUMP; el spawner pot canviar-la abans del primer tick. + anim_ = anim_jump_; +} + +void Dust::setVariant(Variant v) { + anim_ = (v == Variant::JUMP) ? anim_jump_ : anim_land_; + if (anim_.has_value()) { anim_->reset(); } +} + +void Dust::update(float delta_time) { + Entity::update(delta_time); + if (anim_.has_value() && anim_->completed()) { markDead(); } +} + +REGISTER_ENTITY("dust", Dust) diff --git a/source/game/entities/concrete/dust.hpp b/source/game/entities/concrete/dust.hpp new file mode 100644 index 0000000..169ed14 --- /dev/null +++ b/source/game/entities/concrete/dust.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "external/fkyaml_node.hpp" +#include "game/entities/animation_component.hpp" +#include "game/entities/entity.hpp" + +// Polvet transitori: efecte purament visual que viu una animació i +// s'autoelimina (`markDead`) quan acaba. Spawneja el `GameScene` com a +// reacció a events `player_jumped` i `player_landed` del bus. Les dues +// variants (despegue / aterratge) viuen al mateix YAML amb dues +// animacions distintes (`jump` i `land`); l'instància tria una via +// `setVariant` just després de construir-se. +class Dust : public Entity { + public: + enum class Variant : std::uint8_t { + JUMP, + LAND, + }; + + explicit Dust(const fkyaml::node& yaml); + + // Tria quina animació es reproduirà. Cal cridar-lo abans del primer + // `update`; per defecte la variant és JUMP. + void setVariant(Variant v); + + // Tickeja l'animació activa. Quan arriba a `completed()`, marquem + // l'entitat morta perquè l'`EntityManager` la purgue al final del + // frame. + void update(float delta_time) override; + + private: + AnimationComponent anim_jump_; + AnimationComponent anim_land_; +}; diff --git a/source/game/entities/entity.hpp b/source/game/entities/entity.hpp index ca7a1c5..6d034ec 100644 --- a/source/game/entities/entity.hpp +++ b/source/game/entities/entity.hpp @@ -88,6 +88,14 @@ class Entity { [[nodiscard]] auto collides() const -> bool { return collides_; } [[nodiscard]] auto interactable() const -> bool { return interactable_; } + // Marca per a entitats transitòries: efectes (polvet, espurnes, + // explosions...) que viuen una sola animació i s'autoeliminen quan + // acaba. L'`EntityManager` purga totes les entitats amb `dead_=true` + // al final de cada `update`. Les entitats persistents (sarcs, momies, + // portes, jugadors) mai no la posen i hi viuen sempre. + [[nodiscard]] auto isDead() const -> bool { return dead_; } + void markDead() { dead_ = true; } + // Associar el bus una sola vegada (propietat externa, p.ex. // `EntityManager`). Entitats que no publiquen events (Torch, trampes // estàtiques de moment) el poden ignorar. @@ -162,6 +170,7 @@ class Entity { bool collides_{false}; bool interactable_{false}; int height_{0}; + bool dead_{false}; SDL_FlipMode flip_{SDL_FLIP_NONE}; EventBus* bus_{nullptr}; }; diff --git a/source/game/entities/entity_manager.cpp b/source/game/entities/entity_manager.cpp index dbfeed7..01125fd 100644 --- a/source/game/entities/entity_manager.cpp +++ b/source/game/entities/entity_manager.cpp @@ -64,7 +64,16 @@ auto EntityManager::addSarcophagus(std::unique_ptr sarc) -> Sarcoph } void EntityManager::update(float delta_time) { + // ATENCIÓ: si en el futur una entitat publica al bus events que + // resulten en `entity_manager_.add()` (push_back a `entities_`), açò + // invalidaria l'iterador de la ranged-for. Avui no passa: els spawns + // runtime venen de fora del cicle d'`update` (p.ex. el polvet del + // saltador es spawneja durant `Player::tick`, abans d'aquest mètode). for (auto& e : entities_) { e->update(delta_time); } + // Purga d'entitats marcades mortes aquest frame (efectes acabats, + // p.ex. polvet d'aterratge). Les persistents (sarcs, momies, etc.) + // mai no es marquen. + std::erase_if(entities_, [](const auto& e) { return e->isDead(); }); } void EntityManager::notifyPlayerTilePainted(int col, int row) { diff --git a/source/game/entities/event_bus.hpp b/source/game/entities/event_bus.hpp index fd7789e..88b8e64 100644 --- a/source/game/entities/event_bus.hpp +++ b/source/game/entities/event_bus.hpp @@ -41,9 +41,17 @@ struct PaintedTile { int tile_index{-1}; }; +// Coordenades en píxels al canvas. Payload genèric per a events que +// volen indicar "una cosa ha passat aquí" — actualment `player_jumped` +// i `player_landed`, que el GameScene escolta per spawnejar polvet. +struct WorldPos { + float x{0.0F}; + float y{0.0F}; +}; + struct Event { std::string name; - using Payload = std::variant; + using Payload = std::variant; Payload payload; // Jugador que ha originat (o provocat) l'event. -1 = sense jugador // associat (events del món, com `sarcophagus_opened` des de la pròpia @@ -90,6 +98,9 @@ class EventBus { void publish(const std::string& name, PaintedTile painted) { publish(Event{.name = name, .payload = painted}); } + void publish(const std::string& name, WorldPos pos) { + publish(Event{.name = name, .payload = pos}); + } private: struct Subscription { diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 5762236..fc97b5c 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -579,6 +579,15 @@ void Player::startJump() { if (!jump_config.sound_key.empty() && Audio::get() != nullptr) { Audio::get()->playSound(jump_config.sound_key, Audio::Group::GAME); } + // Avisem al bus perquè el GameScene puga spawnejar el polvet de + // despegue al peu del jugador. + if (bus_ != nullptr) { + bus_->publish(Event{ + .name = "player_jumped", + .payload = WorldPos{.x = getPosX() + (static_cast(sprite_w) * 0.5F), .y = getGroundFeetY()}, + .player_id = id_, + }); + } // Prou: la velocitat horitzontal del salt és la `intended_v*` actual // (input del frame). El facing també queda com està. Ja no es // tocaran fins a aterrar. @@ -676,6 +685,13 @@ auto Player::integrateJump(float delta_time) -> bool { if (!jump_config.land_sound_key.empty() && Audio::get() != nullptr) { Audio::get()->playSound(jump_config.land_sound_key, Audio::Group::GAME); } + if (bus_ != nullptr) { + bus_->publish(Event{ + .name = "player_landed", + .payload = WorldPos{.x = getPosX() + (static_cast(sprite_w) * 0.5F), .y = getGroundFeetY()}, + .player_id = id_, + }); + } return true; } return false; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 4d5083f..82c21c8 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -26,7 +26,9 @@ #include "core/system/event_buffer.hpp" #include "core/ui/ui_config.hpp" #include "external/fkyaml_node.hpp" +#include "game/entities/concrete/dust.hpp" #include "game/entities/entity.hpp" +#include "game/entities/entity_loader.hpp" #include "game/entities/event_bus.hpp" #include "game/entities/player.hpp" #include "game/map/map.hpp" @@ -273,6 +275,21 @@ GameScene::GameScene(Config config) << " dmg=" << (dmg != nullptr ? *dmg : 0) << '\n'; }); + // Polvet: precarreguem el surface del Dust una sola vegada perquè + // els spawns runtime de l'entitat no facin disc al primer salt. + // També ens subscrivim a `player_jumped` i `player_landed` per + // spawnejar el polvet de despegue / aterratge a la posició dels + // peus que el Player ens dóna al payload. + Resource::Cache::get().loadSurface("dust.gif", "data/entities/dust/dust.gif"); + entity_manager_.getBus().subscribe("player_jumped", [this](const Event& ev) { + const auto* pos = std::get_if(&ev.payload); + if (pos != nullptr) { spawnDust(pos->x, pos->y, false); } + }); + entity_manager_.getBus().subscribe("player_landed", [this](const Event& ev) { + const auto* pos = std::get_if(&ev.payload); + if (pos != nullptr) { spawnDust(pos->x, pos->y, true); } + }); + // Controls per jugador: P1 amb fletxes, P2 amb WASD. Cada Player // escolta les seues pròpies tecles dins del seu `update`, no hi ha // estat global d'input. @@ -382,6 +399,30 @@ GameScene::GameScene(Config config) GameScene::~GameScene() = default; +void GameScene::spawnDust(float feet_x_center, float feet_y, bool is_landing) { + // Mida del sprite de Dust llegida del seu YAML. Hardcoded per + // simplificar — si canvia, només cal tocar dust.yaml + aquesta + // constant. La caixa de render es centra horitzontalment al peu i + // s'alinea al sòl amb un pixel de marge cap amunt perquè la sombra + // quede pintada per davant del polvet i el polvet, en l'y-sort, + // entre per davant del jugador (mateixa lògica que la sombra). + constexpr float DUST_W = 40.0F; + constexpr float DUST_H = 16.0F; + auto dust = std::make_unique(EntityLoader::loadTemplate("dust")); + dust->setVariant(is_landing ? Dust::Variant::LAND : Dust::Variant::JUMP); + dust->setSurface(Resource::Cache::get().getSurface("dust.gif")); + // Recolzat al sòl, amb la base del rect 1 px per sobre del peu del + // jugador → en l'y-sort surt amb feet_y un pèl menor i, per + // estabilitat de stable_sort, queda darrere del jugador. + dust->setPixelRect({ + .x = feet_x_center - (DUST_W * 0.5F), + .y = feet_y - DUST_H - 1.0F, + .w = DUST_W, + .h = DUST_H, + }); + entity_manager_.add(std::move(dust)); +} + void GameScene::handlePauseInput() { #ifdef _DEBUG // Mutex amb l'editor: si està actiu, ignorem la tecla de pausa. diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index a82db53..f982164 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -56,6 +56,12 @@ class GameScene : public Scene { // (no escalat) perquè la transició viu en temps real. void updatePauseTransition(int delta_ms); + // Spawna un Dust transitori centrat a (`feet_x_center`, `feet_y`). + // Cridat per la subscripció del bus a `player_jumped` / + // `player_landed` per pintar el polvet de despegue / aterratge. + // `is_landing` selecciona la variant de l'animació al `Dust`. + void spawnDust(float feet_x_center, float feet_y, bool is_landing); + Config config_; Tilemap tilemap_;