#include "game/entities/concrete/sarcophagus.hpp" #include #include #include #include "core/audio/audio.hpp" #include "core/resources/resource_cache.hpp" #include "game/entities/entity_factory.hpp" #include "game/entities/entity_manager.hpp" #include "game/entities/event_bus.hpp" #include "utils/defaults.hpp" namespace { // RNG local al mòdul, mateix patró que `mummy.cpp`: llavor fixa per // facilitar la depuració. Si algun dia volem aleatoritat real entre // partides, prou amb llegir-la de std::random_device. auto rng() -> std::mt19937& { static std::mt19937 k_rng_{0xCAFEU}; return k_rng_; } auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent { std::vector 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()); } 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}; } // 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(); h = ts[1].get_value(); } } } // 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("sarcophagus"); collides_ = true; interactable_ = true; readTileSize(yaml, tile_size_w_, tile_size_h_); anim_ = anim_closed_; // Sons opcionals (bloc `sound:` del YAML). El basename val tant de // clau al Resource::Cache com de path dins `Defaults::Paths::AUDIO_SOUNDS_DIR`. // Mateix patró que `mummy.hurt.sound` i `player.jump.sound`. if (yaml.contains("sound")) { const auto& s = yaml["sound"]; if (s.contains("opening")) { opening_sound_key_ = s["opening"].get_value(); Resource::Cache::get().loadSound(opening_sound_key_, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + opening_sound_key_); } if (s.contains("opening_pitch_spread")) { opening_pitch_spread_ = s["opening_pitch_spread"].get_value(); } } // Bloc `reward:` opcional — la recompensa que rebrà el jugador que // tanqui el sarcòfag. Si no hi és, els camps queden a 0 i el GameScene // simplement no aplica res al consumir `sarcophagus_opened`. if (yaml.contains("reward")) { const auto& r = yaml["reward"]; if (r.contains("points")) { points_reward_ = r["points"].get_value(); } if (r.contains("treasures")) { treasures_reward_ = r["treasures"].get_value(); } } } 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 d'un rectangle (w,h) de tiles són les files de tiles // immediatament adjacents: N, S, E, O *més* les quatre cantonades // diagonals (NO, NE, SO, SE). En la pràctica el jugador trepitja // aquestes cantonades en doblar les cantonades del sarcòfag, així que // si no s'hi compten, el cerco no es pot tancar visualment encara // que es completi a nivell de codi (o, pitjor, s'obre sense que el // jugador hagi clos realment el contorn). // // - Fila Nord: (col, row-1) per col ∈ [col0-1, col0+w] (inclou NO i NE) // - Fila Sud: (col, row+h) per col ∈ [col0-1, col0+w] (inclou SO i SE) // - 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 (incloent les quatre cantonades diagonals) for (int dc = -1; 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 (les cantonades ja les ha posat el bucle N/S) 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) { // Publica amb `player_id = last_closer_player_id_` perquè el // GameScene pugui atribuir la recompensa al jugador correcte. // El payload manté el punter al sarc perquè el consumidor // pugui llegir `getPointsReward()` i `getTreasuresReward()`. bus_->publish(Event{ .name = "sarcophagus_opened", .payload = static_cast(this), .player_id = last_closer_player_id_, }); opened_event_emitted_ = true; } } } void Sarcophagus::notifyTilePainted(int col, int row, int player_id) { 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()) { // Atribució: el jugador que pinta el tile que tanca el cercle es // queda amb la recompensa. Si el broadcast inicial de càrrega // arribara a tancar un sarc (no hauria de passar perquè els tiles // d'aquest broadcast són decals/murs, no de corredor), `player_id` // seria -1 i el GameScene saltarà el repartiment. last_closer_player_id_ = player_id; state_ = State::OPENING; anim_ = anim_opening_; if (anim_.has_value()) { anim_->reset(); } if (!opening_sound_key_.empty() && Audio::get() != nullptr) { // Variació de pitch/velocitat per evitar la fatiga d'escoltar // el mateix sample idèntic a cada obertura. Si l'spread és 0 // (default), reproduïm amb ratio 1.0; en cas contrari sortegem // uniformement [1 - spread, 1 + spread]. if (opening_pitch_spread_ > 0.0F) { std::uniform_real_distribution dist(1.0F - opening_pitch_spread_, 1.0F + opening_pitch_spread_); Audio::get()->playSound(opening_sound_key_, Audio::Group::GAME, dist(rng())); } else { Audio::get()->playSound(opening_sound_key_, Audio::Group::GAME); } } } } void Sarcophagus::onRemoved() { // El manager comptabilitza els sarcòfags actius en `total_sarcs_` per // saber quan ha d'obrir la porta automàticament. Quan el editor // n'esborra un, cal informar-lo: sense aquesta crida, `total_sarcs_` // quedaria descompassat i la porta no s'obriria mai. Substitueix el // `getName() == "sarcophagus"` que el manager feia abans — // polimorfisme net, no comparació de strings. if (entity_manager_ != nullptr) { entity_manager_->onSarcophagusRemoved(); } } REGISTER_ENTITY("sarcophagus", Sarcophagus)