feat(stats): Player com a font de veritat de vides/score/tresors i sarcs amb recompensa
This commit is contained in:
1 parent
d186f66c2c
commit
f5d251c33b
14 files changed
+226
-34
No files matched your search
@@ -40,3 +40,9 @@ behaviour:
|
|||||||
sound:
|
sound:
|
||||||
opening: open_sarc.wav
|
opening: open_sarc.wav
|
||||||
opening_pitch_spread: 0.5
|
opening_pitch_spread: 0.5
|
||||||
|
# Recompensa que rep el jugador que pinta el tile que tanca el perímetre
|
||||||
|
# (el GameScene la reparteix consumint `sarcophagus_opened`). Si es lleva
|
||||||
|
# aquest bloc o un dels camps, el camp absent val 0 i no s'aplica.
|
||||||
|
reward:
|
||||||
|
points: 100
|
||||||
|
treasures: 1
|
||||||
@@ -83,6 +83,15 @@ Sarcophagus::Sarcophagus(const fkyaml::node& yaml)
|
|||||||
opening_pitch_spread_ = s["opening_pitch_spread"].get_value<float>();
|
opening_pitch_spread_ = s["opening_pitch_spread"].get_value<float>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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<int>(); }
|
||||||
|
if (r.contains("treasures")) { treasures_reward_ = r["treasures"].get_value<int>(); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Sarcophagus::place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked) {
|
void Sarcophagus::place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked) {
|
||||||
@@ -135,18 +144,32 @@ void Sarcophagus::update(float delta_time) {
|
|||||||
anim_ = anim_open_;
|
anim_ = anim_open_;
|
||||||
if (anim_.has_value()) { anim_->reset(); }
|
if (anim_.has_value()) { anim_->reset(); }
|
||||||
if (!opened_event_emitted_ && bus_ != nullptr) {
|
if (!opened_event_emitted_ && bus_ != nullptr) {
|
||||||
bus_->publish("sarcophagus_opened", static_cast<Entity*>(this));
|
// 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<Entity*>(this),
|
||||||
|
.player_id = last_closer_player_id_,
|
||||||
|
});
|
||||||
opened_event_emitted_ = true;
|
opened_event_emitted_ = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Sarcophagus::notifyTilePainted(int col, int row) {
|
void Sarcophagus::notifyTilePainted(int col, int row, int player_id) {
|
||||||
if (state_ != State::CLOSED) { return; }
|
if (state_ != State::CLOSED) { return; }
|
||||||
const TileKey KEY{.col = col, .row = row};
|
const TileKey KEY{.col = col, .row = row};
|
||||||
if (perimeter_tiles_.find(KEY) == perimeter_tiles_.end()) { return; }
|
if (perimeter_tiles_.find(KEY) == perimeter_tiles_.end()) { return; }
|
||||||
visited_.insert(KEY);
|
visited_.insert(KEY);
|
||||||
if (visited_.size() >= perimeter_tiles_.size()) {
|
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;
|
state_ = State::OPENING;
|
||||||
anim_ = anim_opening_;
|
anim_ = anim_opening_;
|
||||||
if (anim_.has_value()) { anim_->reset(); }
|
if (anim_.has_value()) { anim_->reset(); }
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class Sarcophagus : public Interactable {
|
|||||||
void place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked);
|
void place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked);
|
||||||
|
|
||||||
void update(float delta_time) override;
|
void update(float delta_time) override;
|
||||||
void notifyTilePainted(int col, int row) override;
|
void notifyTilePainted(int col, int row, int player_id) override;
|
||||||
void onRemoved() override;
|
void onRemoved() override;
|
||||||
|
|
||||||
[[nodiscard]] auto getState() const -> State { return state_; }
|
[[nodiscard]] auto getState() const -> State { return state_; }
|
||||||
@@ -58,6 +58,17 @@ class Sarcophagus : public Interactable {
|
|||||||
[[nodiscard]] auto getTileWidth() const -> int { return tile_size_w_; }
|
[[nodiscard]] auto getTileWidth() const -> int { return tile_size_w_; }
|
||||||
[[nodiscard]] auto getTileHeight() const -> int { return tile_size_h_; }
|
[[nodiscard]] auto getTileHeight() const -> int { return tile_size_h_; }
|
||||||
|
|
||||||
|
// Recompensa que es donarà al jugador que pinta el tile que tanca el
|
||||||
|
// perímetre (el `last_closer_player_id_`). Llegits del YAML del sarc;
|
||||||
|
// 0/0 si el bloc `reward:` no existeix. El GameScene escolta
|
||||||
|
// `sarcophagus_opened` i aplica aquests valors al Player corresponent.
|
||||||
|
[[nodiscard]] auto getPointsReward() const -> int { return points_reward_; }
|
||||||
|
[[nodiscard]] auto getTreasuresReward() const -> int { return treasures_reward_; }
|
||||||
|
// Jugador atribuit (qui pinta el tile que dispara CLOSED→OPENING). -1
|
||||||
|
// si el sarcòfag s'ha tancat per pintades sense atribució (p.ex. el
|
||||||
|
// broadcast inicial de càrrega) — cas que el GameScene ignora.
|
||||||
|
[[nodiscard]] auto getLastCloserPlayerId() const -> int { return last_closer_player_id_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct TileKey {
|
struct TileKey {
|
||||||
int col;
|
int col;
|
||||||
@@ -94,5 +105,16 @@ class Sarcophagus : public Interactable {
|
|||||||
// obertura sorteja [1 - spread, 1 + spread]. 0 ⇒ ratio fix a 1.0.
|
// obertura sorteja [1 - spread, 1 + spread]. 0 ⇒ ratio fix a 1.0.
|
||||||
float opening_pitch_spread_{0.0F};
|
float opening_pitch_spread_{0.0F};
|
||||||
|
|
||||||
|
// Recompenses llegides del YAML (bloc `reward:`). 0/0 si el bloc no
|
||||||
|
// hi és — el GameScene salta el pas de repartiment quan són 0.
|
||||||
|
int points_reward_{0};
|
||||||
|
int treasures_reward_{0};
|
||||||
|
|
||||||
|
// Jugador que ha pintat el tile que tanca el perímetre. Es fixa una
|
||||||
|
// sola vegada, en la transició CLOSED→OPENING dins de notifyTilePainted.
|
||||||
|
// -1 mentre està a CLOSED, o si la pintada que el tanca no porta
|
||||||
|
// atribució (player_id == -1 al broadcast inicial).
|
||||||
|
int last_closer_player_id_{-1};
|
||||||
|
|
||||||
bool opened_event_emitted_{false};
|
bool opened_event_emitted_{false};
|
||||||
};
|
};
|
||||||
@@ -24,7 +24,7 @@ void Entity::onPlayerContact(Player* /*player*/) {
|
|||||||
// No-op per defecte.
|
// No-op per defecte.
|
||||||
}
|
}
|
||||||
|
|
||||||
void Entity::notifyTilePainted(int /*col*/, int /*row*/) {
|
void Entity::notifyTilePainted(int /*col*/, int /*row*/, int /*player_id*/) {
|
||||||
// No-op per defecte; només `Sarcophagus` l'usa.
|
// No-op per defecte; només `Sarcophagus` l'usa.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,8 +84,12 @@ class Entity {
|
|||||||
// Hook que el `EntityManager` invoca per a cada entitat cada vegada
|
// Hook que el `EntityManager` invoca per a cada entitat cada vegada
|
||||||
// que un tile es pinta (via PaintLayer, com a conseqüència d'un
|
// que un tile es pinta (via PaintLayer, com a conseqüència d'un
|
||||||
// jugador trepitjant un sòl pintable). Només `Sarcophagus` ho consumeix
|
// jugador trepitjant un sòl pintable). Només `Sarcophagus` ho consumeix
|
||||||
// (per marcar perímetre); per defecte no-op.
|
// (per marcar perímetre); per defecte no-op. `player_id` és l'id del
|
||||||
virtual void notifyTilePainted(int col, int row);
|
// jugador que ha pintat el tile, o -1 si la pintada ve del broadcast
|
||||||
|
// inicial de càrrega de nivell (decals + murs, sense atribució). Els
|
||||||
|
// consumidors han de filtrar per `player_id >= 0` si necessiten
|
||||||
|
// atribució.
|
||||||
|
virtual void notifyTilePainted(int col, int row, int player_id);
|
||||||
|
|
||||||
// L'entitat ha rebut un fueteig amb solapament. Es passa el punter al
|
// L'entitat ha rebut un fueteig amb solapament. Es passa el punter al
|
||||||
// `WhipAttack` perquè la subclasse pugui llegir-ne direcció o atributs
|
// `WhipAttack` perquè la subclasse pugui llegir-ne direcció o atributs
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ EntityManager::EntityManager() {
|
|||||||
// els sarcòfags puguin actualitzar el seu `visited_`.
|
// els sarcòfags puguin actualitzar el seu `visited_`.
|
||||||
tile_sub_id_ = bus_.subscribe("tile_painted", [this](const Event& ev) {
|
tile_sub_id_ = bus_.subscribe("tile_painted", [this](const Event& ev) {
|
||||||
const auto* tile = std::get_if<TilePos>(&ev.payload);
|
const auto* tile = std::get_if<TilePos>(&ev.payload);
|
||||||
if (tile != nullptr) { notifyPlayerTilePainted(tile->col, tile->row); }
|
if (tile != nullptr) { notifyPlayerTilePainted(tile->col, tile->row, ev.player_id); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Subscripció a `player_exited` (publicat per Door en travessar la
|
// Subscripció a `player_exited` (publicat per Door en travessar la
|
||||||
@@ -141,12 +141,13 @@ void EntityManager::update(float delta_time) {
|
|||||||
std::erase_if(entities_, [](const auto& e) { return e->isDead(); });
|
std::erase_if(entities_, [](const auto& e) { return e->isDead(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityManager::notifyPlayerTilePainted(int col, int row) {
|
void EntityManager::notifyPlayerTilePainted(int col, int row, int player_id) {
|
||||||
// Fan-out polimòrfic a totes les entitats: cada una decideix què fer
|
// Fan-out polimòrfic a totes les entitats: cada una decideix què fer
|
||||||
// (`Sarcophagus` actualitza el seu perímetre, la resta queden en
|
// (`Sarcophagus` actualitza el seu perímetre i recorda qui va tancar
|
||||||
// no-op). Cost d'iterar 40 elements amb crida virtual buida ~ 1 µs;
|
// el cercle, la resta queden en no-op). `player_id` ve del bus
|
||||||
// qui paga el cost guanya simplicitat al callsite.
|
// (`ev.player_id`) — pot ser -1 al broadcast de càrrega inicial de
|
||||||
for (auto& e : entities_) { e->notifyTilePainted(col, row); }
|
// nivell, els consumidors filtren si necessiten atribució.
|
||||||
|
for (auto& e : entities_) { e->notifyTilePainted(col, row, player_id); }
|
||||||
}
|
}
|
||||||
|
|
||||||
auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* {
|
auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* {
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ class DynamicHazard;
|
|||||||
// 2. `update(dt)` propaga el tick a totes.
|
// 2. `update(dt)` propaga el tick a totes.
|
||||||
// 3. Comptador de sarcòfags oberts — quan iguala el total registrat,
|
// 3. Comptador de sarcòfags oberts — quan iguala el total registrat,
|
||||||
// crida `door_->open()` automàticament.
|
// crida `door_->open()` automàticament.
|
||||||
// 4. `notifyPlayerTilePainted(col, row)` — fan-out del hook per a tots
|
// 4. `notifyPlayerTilePainted(col, row, player_id)` — fan-out del hook
|
||||||
// els sarcòfags (i qualsevol entitat futura que s'hi subscrigui via
|
// per a tots els sarcòfags (i qualsevol entitat futura que s'hi
|
||||||
// `notifyTilePainted`).
|
// subscrigui via `notifyTilePainted`). El `player_id` permet
|
||||||
|
// atribuir l'acció al jugador concret (p.ex. qui tanca un sarc).
|
||||||
//
|
//
|
||||||
// NO és un singleton: es crea una instància per nivell. De moment no
|
// NO és un singleton: es crea una instància per nivell. De moment no
|
||||||
// s'usa encara al GameScene (andamiatge pur).
|
// s'usa encara al GameScene (andamiatge pur).
|
||||||
@@ -103,7 +104,7 @@ class EntityManager {
|
|||||||
// notifyTilePainted. Les entitats no-sarcòfag ho ignoren per defecte
|
// notifyTilePainted. Les entitats no-sarcòfag ho ignoren per defecte
|
||||||
// (veure Entity::notifyTilePainted). Invocat com a reacció a
|
// (veure Entity::notifyTilePainted). Invocat com a reacció a
|
||||||
// `tile_painted` al bus (re-publicat pel PaintLayer un cop filtrat).
|
// `tile_painted` al bus (re-publicat pel PaintLayer un cop filtrat).
|
||||||
void notifyPlayerTilePainted(int col, int row);
|
void notifyPlayerTilePainted(int col, int row, int player_id);
|
||||||
|
|
||||||
// Carrega els spawns d'un YAML de nivell i els afegeix al manager:
|
// Carrega els spawns d'un YAML de nivell i els afegeix al manager:
|
||||||
// 1. Llegeix el bloc `entities:` via EntityLoader.
|
// 1. Llegeix el bloc `entities:` via EntityLoader.
|
||||||
|
|||||||
@@ -19,12 +19,21 @@ class Player;
|
|||||||
// Event pub/sub minimalista. Hi ha un únic `EventBus` per mundus (propietat
|
// Event pub/sub minimalista. Hi ha un únic `EventBus` per mundus (propietat
|
||||||
// de l'`EntityManager`); les entitats que el necessiten en reben un punter
|
// de l'`EntityManager`); les entitats que el necessiten en reben un punter
|
||||||
// via `Entity::setEventBus`. Aquest sistema cobreix les necessitats
|
// via `Entity::setEventBus`. Aquest sistema cobreix les necessitats
|
||||||
// actuals (sarcophagus_opened, level_complete, player_hit, on_tile_entered)
|
// actuals (sarcophagus_opened, level_complete, player_hit, on_tile_entered,
|
||||||
// sense portar-hi senyalització més pesada.
|
// player_stats_changed) sense portar-hi senyalització més pesada.
|
||||||
//
|
//
|
||||||
// Decisió: `std::variant` per al payload — evita any/void* i facilita
|
// Decisió: `std::variant` per al payload — evita any/void* i facilita
|
||||||
// consumir events tipats amb `std::get_if`. Si apareixen més tipus de
|
// consumir events tipats amb `std::get_if`. Si apareixen més tipus de
|
||||||
// payload, s'amplia aquí sense tocar cridants existents.
|
// payload, s'amplia aquí sense tocar cridants existents.
|
||||||
|
//
|
||||||
|
// `player_stats_changed`: l'únic canal numèric que toca el HUD. El `Player`
|
||||||
|
// és la font de veritat de vides/score/tresors i el publica cada vegada
|
||||||
|
// que muta qualsevol d'aquests camps. El HUD el consumeix com a "snapshot
|
||||||
|
// total" (no com a delta) — sobreescriu la seua caché amb el payload. Així
|
||||||
|
// qualsevol via que modifique stats al Player (mort per forat, recompensa
|
||||||
|
// de sarc, etc.) queda automàticament reflectida sense que cada caller
|
||||||
|
// haja de recordar publicar res addicional. `player_hit` queda només per
|
||||||
|
// al feedback visual (flash/shake), separat del comptador.
|
||||||
|
|
||||||
// Tile (col, row) — payload de `tile_painted` (i altres events de tile sense
|
// Tile (col, row) — payload de `tile_painted` (i altres events de tile sense
|
||||||
// metadada extra). No és un `pair<int,int>` per evitar alias ambigus al
|
// metadada extra). No és un `pair<int,int>` per evitar alias ambigus al
|
||||||
@@ -74,9 +83,20 @@ struct WhipSpawnPayload {
|
|||||||
float range_px{0.0F};
|
float range_px{0.0F};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Snapshot complet dels stats d'un jugador — payload de `player_stats_changed`.
|
||||||
|
// El Player publica un d'aquests cada vegada que muta vides/score/tresors.
|
||||||
|
// El HUD el consumeix com a estat absolut (no delta): sobreescriu la seua
|
||||||
|
// caché amb aquests valors. El `player_id` de l'`Event` indica a quin
|
||||||
|
// bloc del HUD va dirigit.
|
||||||
|
struct PlayerStatsPayload {
|
||||||
|
int lives{0};
|
||||||
|
int score{0};
|
||||||
|
int treasures{0};
|
||||||
|
};
|
||||||
|
|
||||||
struct Event {
|
struct Event {
|
||||||
std::string name;
|
std::string name;
|
||||||
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile, WorldPos, WhipSpawnPayload>;
|
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile, WorldPos, WhipSpawnPayload, PlayerStatsPayload>;
|
||||||
Payload payload;
|
Payload payload;
|
||||||
// Jugador que ha originat (o provocat) l'event. -1 = sense jugador
|
// Jugador que ha originat (o provocat) l'event. -1 = sense jugador
|
||||||
// associat (events del món, com `sarcophagus_opened` des de la pròpia
|
// associat (events del món, com `sarcophagus_opened` des de la pròpia
|
||||||
@@ -129,6 +149,9 @@ class EventBus {
|
|||||||
void publish(const std::string& name, const WhipSpawnPayload& whip) {
|
void publish(const std::string& name, const WhipSpawnPayload& whip) {
|
||||||
publish(Event{.name = name, .payload = whip});
|
publish(Event{.name = name, .payload = whip});
|
||||||
}
|
}
|
||||||
|
void publish(const std::string& name, const PlayerStatsPayload& stats) {
|
||||||
|
publish(Event{.name = name, .payload = stats});
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Subscription {
|
struct Subscription {
|
||||||
|
|||||||
@@ -1272,6 +1272,37 @@ void Player::applyDyingMovement(float delta_time) {
|
|||||||
|
|
||||||
void Player::resetForJoin() {
|
void Player::resetForJoin() {
|
||||||
lives_remaining_ = death_config.initial_lives;
|
lives_remaining_ = death_config.initial_lives;
|
||||||
|
score_ = 0;
|
||||||
|
treasures_ = 0;
|
||||||
|
// Publica l'estat inicial perquè el HUD reompla el bloc en el moment
|
||||||
|
// de fer join (no només quan el comptador canvia després). Sense açò
|
||||||
|
// el HUD es quedaria amb la caché prèvia fins al primer event.
|
||||||
|
publishStatsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Player::publishStatsChanged() {
|
||||||
|
if (bus_ == nullptr) { return; }
|
||||||
|
bus_->publish(Event{
|
||||||
|
.name = "player_stats_changed",
|
||||||
|
.payload = PlayerStatsPayload{
|
||||||
|
.lives = lives_remaining_,
|
||||||
|
.score = score_,
|
||||||
|
.treasures = treasures_,
|
||||||
|
},
|
||||||
|
.player_id = id_,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Player::addScore(int delta) {
|
||||||
|
if (delta == 0) { return; }
|
||||||
|
score_ += delta;
|
||||||
|
publishStatsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Player::addTreasures(int delta) {
|
||||||
|
if (delta == 0) { return; }
|
||||||
|
treasures_ += delta;
|
||||||
|
publishStatsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::respawnFromDying() {
|
void Player::respawnFromDying() {
|
||||||
@@ -1283,6 +1314,12 @@ void Player::respawnFromDying() {
|
|||||||
--lives_remaining_;
|
--lives_remaining_;
|
||||||
death_animation_complete_ = false;
|
death_animation_complete_ = false;
|
||||||
respawn_remaining_ms_ = 0.0F;
|
respawn_remaining_ms_ = 0.0F;
|
||||||
|
// Notifica al HUD del nou comptador de vides abans de qualsevol
|
||||||
|
// transició d'estat. Aquesta crida tanca el bucle de qualsevol via
|
||||||
|
// de mort (mòmia, traps, rolling rock, FORAT…): totes acaben aquí, i
|
||||||
|
// ningú més ha de recordar publicar res. Era exactament el punt que
|
||||||
|
// s'oblidava al camí del forat.
|
||||||
|
publishStatsChanged();
|
||||||
if (lives_remaining_ <= 0) {
|
if (lives_remaining_ <= 0) {
|
||||||
disable();
|
disable();
|
||||||
if (bus_ != nullptr) {
|
if (bus_ != nullptr) {
|
||||||
|
|||||||
@@ -158,6 +158,21 @@ class Player : public AnimatedSprite {
|
|||||||
// xarxa — és únic per dins del procés.
|
// xarxa — és únic per dins del procés.
|
||||||
[[nodiscard]] auto getId() const -> int { return id_; }
|
[[nodiscard]] auto getId() const -> int { return id_; }
|
||||||
|
|
||||||
|
// Stats que pinta el HUD. El Player n'és la font de veritat; cada
|
||||||
|
// mutació es publica via `player_stats_changed` al bus (vegeu
|
||||||
|
// `publishStatsChanged`). Els getters són per a tests i diagnòstic;
|
||||||
|
// el HUD no els consulta — escolta el bus.
|
||||||
|
[[nodiscard]] auto getLives() const -> int { return lives_remaining_; }
|
||||||
|
[[nodiscard]] auto getScore() const -> int { return score_; }
|
||||||
|
[[nodiscard]] auto getTreasures() const -> int { return treasures_; }
|
||||||
|
|
||||||
|
// Acumuladors. Deltas positius i negatius (sense clamp). Publiquen
|
||||||
|
// `player_stats_changed` només si el delta no és 0, per estalviar
|
||||||
|
// soroll al bus. Els consumidors típics: GameScene reparteix puntuació
|
||||||
|
// de sarcs i pickups (futur) cridant aquests mètodes.
|
||||||
|
void addScore(int delta);
|
||||||
|
void addTreasures(int delta);
|
||||||
|
|
||||||
// Accés i mutació de la velocitat intencionada d'aquest frame. El
|
// Accés i mutació de la velocitat intencionada d'aquest frame. El
|
||||||
// GameScene els toca entre `tick` i `applyMovement` per implementar
|
// GameScene els toca entre `tick` i `applyMovement` per implementar
|
||||||
// col·lisions jugador↔jugador (escalar a push_factor, anul·lar en
|
// col·lisions jugador↔jugador (escalar a push_factor, anul·lar en
|
||||||
@@ -852,6 +867,17 @@ class Player : public AnimatedSprite {
|
|||||||
// inicial quan el jugador re-entra mid-partida.
|
// inicial quan el jugador re-entra mid-partida.
|
||||||
int lives_remaining_{0};
|
int lives_remaining_{0};
|
||||||
|
|
||||||
|
// Puntuació acumulada i nombre de tresors recollits. Es modifiquen
|
||||||
|
// via `addScore` / `addTreasures` (o reset a `resetForJoin`). El HUD
|
||||||
|
// els sincronitza via `player_stats_changed`.
|
||||||
|
int score_{0};
|
||||||
|
int treasures_{0};
|
||||||
|
|
||||||
|
// Publica un snapshot complet (vides/score/tresors) al bus si està
|
||||||
|
// injectat. Centralitza la generació de l'event perquè cap mutador
|
||||||
|
// se l'oblide.
|
||||||
|
void publishStatsChanged();
|
||||||
|
|
||||||
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
||||||
// sprite). Les assigna el GameScene via `setSpawnPoint`. 0/0 fins que
|
// sprite). Les assigna el GameScene via `setSpawnPoint`. 0/0 fins que
|
||||||
// el GameScene n'invoque el setter al ctor / `loadRoom`.
|
// el GameScene n'invoque el setter al ctor / `loadRoom`.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "game/entities/concrete/dust.hpp"
|
#include "game/entities/concrete/dust.hpp"
|
||||||
|
#include "game/entities/concrete/sarcophagus.hpp"
|
||||||
#include "game/entities/concrete/whip_attack.hpp"
|
#include "game/entities/concrete/whip_attack.hpp"
|
||||||
#include "game/entities/entity.hpp"
|
#include "game/entities/entity.hpp"
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
@@ -321,6 +322,12 @@ GameScene::GameScene(Config config)
|
|||||||
entity_manager_.getBus().subscribe("player_disabled", [this](const Event& ev) {
|
entity_manager_.getBus().subscribe("player_disabled", [this](const Event& ev) {
|
||||||
onPlayerDisabled(ev.player_id);
|
onPlayerDisabled(ev.player_id);
|
||||||
});
|
});
|
||||||
|
// `sarcophagus_opened`: extret a `onSarcophagusOpened` per acotar
|
||||||
|
// la complexitat cognitiva del ctor (mateix patró que `player_killed`
|
||||||
|
// i `player_disabled`).
|
||||||
|
entity_manager_.getBus().subscribe("sarcophagus_opened", [this](const Event& ev) {
|
||||||
|
onSarcophagusOpened(ev);
|
||||||
|
});
|
||||||
|
|
||||||
// Polvet: precarreguem el surface del Dust una sola vegada perquè
|
// Polvet: precarreguem el surface del Dust una sola vegada perquè
|
||||||
// els spawns runtime de l'entitat no facin disc al primer salt.
|
// els spawns runtime de l'entitat no facin disc al primer salt.
|
||||||
@@ -732,6 +739,21 @@ void GameScene::onPlayerDisabled(int player_id) {
|
|||||||
players_active_[player_id] = false;
|
players_active_[player_id] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameScene::onSarcophagusOpened(const Event& ev) {
|
||||||
|
// `player_id < 0`: pintada sense atribució (broadcast inicial). No
|
||||||
|
// hauria de tancar mai un sarc, però per defensa ignorem.
|
||||||
|
if (ev.player_id < 0) { return; }
|
||||||
|
const auto IDX = static_cast<std::size_t>(ev.player_id);
|
||||||
|
if (IDX >= players_.size()) { return; }
|
||||||
|
const auto* ent = std::get_if<Entity*>(&ev.payload);
|
||||||
|
if (ent == nullptr || *ent == nullptr) { return; }
|
||||||
|
auto* sarc = dynamic_cast<Sarcophagus*>(*ent);
|
||||||
|
if (sarc == nullptr) { return; }
|
||||||
|
auto& p = *players_[IDX];
|
||||||
|
p.addScore(sarc->getPointsReward());
|
||||||
|
p.addTreasures(sarc->getTreasuresReward());
|
||||||
|
}
|
||||||
|
|
||||||
void GameScene::preloadGameOverAssets() const {
|
void GameScene::preloadGameOverAssets() const {
|
||||||
// Pre-load del so del countdown: el dispara `updateGameOverFlow`
|
// Pre-load del so del countdown: el dispara `updateGameOverFlow`
|
||||||
// cada vegada que canvia el número visible de CONTINUE? (incloent
|
// cada vegada que canvia el número visible de CONTINUE? (incloent
|
||||||
|
|||||||
@@ -127,6 +127,14 @@ class GameScene : public Scene {
|
|||||||
// mètode per acotar la complexitat cognitiva del subscriber/ctor.
|
// mètode per acotar la complexitat cognitiva del subscriber/ctor.
|
||||||
void onPlayerDisabled(int player_id);
|
void onPlayerDisabled(int player_id);
|
||||||
|
|
||||||
|
// Handler de l'event `sarcophagus_opened` (un jugador acaba de
|
||||||
|
// tancar el perímetre). Reparteix la recompensa (`points`,
|
||||||
|
// `treasures` del YAML del sarc) al Player corresponent, que al
|
||||||
|
// mutar score/treasures publicarà `player_stats_changed` i el HUD
|
||||||
|
// s'actualitzarà sol. Extret a mètode per acotar la complexitat
|
||||||
|
// cognitiva del ctor.
|
||||||
|
void onSarcophagusOpened(const Event& ev);
|
||||||
|
|
||||||
// Pre-load dels recursos d'àudio que s'usen mid-partida i no els
|
// Pre-load dels recursos d'àudio que s'usen mid-partida i no els
|
||||||
// toca el Director (so del countdown, etc.). Si la clau és buida
|
// toca el Director (so del countdown, etc.). Si la clau és buida
|
||||||
// o ja està al cache, és no-op. Extret a mètode per acotar la
|
// o ja està al cache, és no-op. Extret a mètode per acotar la
|
||||||
|
|||||||
+28
-9
@@ -135,10 +135,14 @@ Hud::Hud(EventBus& bus, std::shared_ptr<Text> font, int initial_lives)
|
|||||||
players_[1].active = false;
|
players_[1].active = false;
|
||||||
players_[1].lives = initial_lives_;
|
players_[1].lives = initial_lives_;
|
||||||
|
|
||||||
// Subscripcions al bus de l'EntityManager. De moment només `player_hit`
|
// Subscripcions al bus de l'EntityManager.
|
||||||
// afecta el HUD visualment: decrementa vides, dispara flash i shake al
|
//
|
||||||
// bloc del jugador. Quan els events evolucionin per portar `player_id`
|
// `player_hit` només fa feedback visual (flash + shake) al bloc del
|
||||||
// i puntuacions, ampliarem aquí sense tocar callers.
|
// jugador colpejat. **No** toca el comptador de vides — la veritat
|
||||||
|
// viu al Player i arriba per `player_stats_changed`. Així si un hit
|
||||||
|
// no és mortal (HURT recoverable) el flash es manté coherent sense
|
||||||
|
// mentir el comptador, i si és mortal el comptador baixa pel canal
|
||||||
|
// numèric quan el cicle DYING acaba a `respawnFromDying`.
|
||||||
subs_.push_back(bus_.subscribe("player_hit", [this](const Event& ev) {
|
subs_.push_back(bus_.subscribe("player_hit", [this](const Event& ev) {
|
||||||
const auto* dmg = std::get_if<int>(&ev.payload);
|
const auto* dmg = std::get_if<int>(&ev.payload);
|
||||||
// Dispatch pel `player_id` de l'event. 0 = P1 (bloc LEFT), 1 = P2
|
// Dispatch pel `player_id` de l'event. 0 = P1 (bloc LEFT), 1 = P2
|
||||||
@@ -147,14 +151,28 @@ Hud::Hud(EventBus& bus, std::shared_ptr<Text> font, int initial_lives)
|
|||||||
// publishers de `player_hit` actuals sí que porten id.
|
// publishers de `player_hit` actuals sí que porten id.
|
||||||
const std::size_t IDX = (ev.player_id == 1) ? 1U : 0U;
|
const std::size_t IDX = (ev.player_id == 1) ? 1U : 0U;
|
||||||
if (!players_[IDX].active) { return; }
|
if (!players_[IDX].active) { return; }
|
||||||
if (players_[IDX].lives > 0) { --players_[IDX].lives; }
|
|
||||||
const std::size_t BLK_IDX = (IDX == 0) ? static_cast<std::size_t>(BlockId::LEFT)
|
const std::size_t BLK_IDX = (IDX == 0) ? static_cast<std::size_t>(BlockId::LEFT)
|
||||||
: static_cast<std::size_t>(BlockId::RIGHT);
|
: static_cast<std::size_t>(BlockId::RIGHT);
|
||||||
auto& blk = blocks_[BLK_IDX];
|
auto& blk = blocks_[BLK_IDX];
|
||||||
blk.flash_ms_left = UiConfig::get().hudFlashMs();
|
blk.flash_ms_left = UiConfig::get().hudFlashMs();
|
||||||
blk.shake_ms_left = UiConfig::get().hudShakeMs();
|
blk.shake_ms_left = UiConfig::get().hudShakeMs();
|
||||||
blk.shake_intensity = 1.5F;
|
blk.shake_intensity = 1.5F;
|
||||||
Log::info("HUD", "player_hit p{} dmg={} lives={}", IDX + 1, dmg != nullptr ? *dmg : 0, players_[IDX].lives);
|
Log::info("HUD", "player_hit p{} dmg={}", IDX + 1, dmg != nullptr ? *dmg : 0);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// `player_stats_changed`: snapshot total (vides/score/tresors) emès
|
||||||
|
// pel Player cada vegada que muta qualsevol dels tres. El HUD el
|
||||||
|
// consumeix com a estat absolut: sobreescriu la caché local sense
|
||||||
|
// raonament addicional. Això unifica totes les fonts de canvi (mort
|
||||||
|
// per qualsevol causa, recompensa de sarc, futurs pickups…) en un
|
||||||
|
// únic canal.
|
||||||
|
subs_.push_back(bus_.subscribe("player_stats_changed", [this](const Event& ev) {
|
||||||
|
const auto* st = std::get_if<PlayerStatsPayload>(&ev.payload);
|
||||||
|
if (st == nullptr) { return; }
|
||||||
|
const std::size_t IDX = (ev.player_id == 1) ? 1U : 0U;
|
||||||
|
players_[IDX].lives = st->lives;
|
||||||
|
players_[IDX].score = st->score;
|
||||||
|
players_[IDX].treasures = st->treasures;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
subs_.push_back(bus_.subscribe("level_complete", [](const Event& /*ev*/) {
|
subs_.push_back(bus_.subscribe("level_complete", [](const Event& /*ev*/) {
|
||||||
@@ -207,10 +225,11 @@ void Hud::activatePlayer(int player_one_based) {
|
|||||||
if (player_one_based < 1 || player_one_based > 2) { return; }
|
if (player_one_based < 1 || player_one_based > 2) { return; }
|
||||||
auto& p = players_[static_cast<std::size_t>(player_one_based - 1)];
|
auto& p = players_[static_cast<std::size_t>(player_one_based - 1)];
|
||||||
p.active = true;
|
p.active = true;
|
||||||
p.lives = initial_lives_;
|
|
||||||
p.score = 0;
|
|
||||||
p.treasures = 0;
|
|
||||||
p.join_prompt = JoinPromptMode::NONE;
|
p.join_prompt = JoinPromptMode::NONE;
|
||||||
|
// Els camps numèrics (lives/score/treasures) ja els ha sobreescrit el
|
||||||
|
// `player_stats_changed` que el Player publica dins de `resetForJoin`,
|
||||||
|
// així que aquí no els toquem — evitem trepitjar la veritat del bus
|
||||||
|
// amb defaults locals.
|
||||||
}
|
}
|
||||||
|
|
||||||
void Hud::setJoinPrompt(int player_one_based, JoinPromptMode mode) {
|
void Hud::setJoinPrompt(int player_one_based, JoinPromptMode mode) {
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ namespace {
|
|||||||
REQUIRE(sarc->getState() == Sarcophagus::State::CLOSED);
|
REQUIRE(sarc->getState() == Sarcophagus::State::CLOSED);
|
||||||
|
|
||||||
// Visitem un tile que NO és al perímetre: res canvia.
|
// Visitem un tile que NO és al perímetre: res canvia.
|
||||||
sarc->notifyTilePainted(5, 5);
|
sarc->notifyTilePainted(5, 5, /*player_id=*/0);
|
||||||
REQUIRE(sarc->getVisitedCount() == 0);
|
REQUIRE(sarc->getVisitedCount() == 0);
|
||||||
|
|
||||||
// Visitem tots els del perímetre.
|
// Visitem tots els del perímetre.
|
||||||
@@ -166,7 +166,7 @@ namespace {
|
|||||||
{0, 2}, {1, 2}, {2, 2}, {3, 2}, {4, 2},
|
{0, 2}, {1, 2}, {2, 2}, {3, 2}, {4, 2},
|
||||||
{0, 1}, {4, 1},
|
{0, 1}, {4, 1},
|
||||||
};
|
};
|
||||||
for (const auto& [c, r] : PERIM) { sarc->notifyTilePainted(c, r); }
|
for (const auto& [c, r] : PERIM) { sarc->notifyTilePainted(c, r, /*player_id=*/0); }
|
||||||
REQUIRE(sarc->getVisitedCount() == 12);
|
REQUIRE(sarc->getVisitedCount() == 12);
|
||||||
REQUIRE(sarc->getState() == Sarcophagus::State::OPENING);
|
REQUIRE(sarc->getState() == Sarcophagus::State::OPENING);
|
||||||
|
|
||||||
@@ -204,12 +204,12 @@ namespace {
|
|||||||
const int W = s->getTileWidth();
|
const int W = s->getTileWidth();
|
||||||
const int H = s->getTileHeight();
|
const int H = s->getTileHeight();
|
||||||
for (int dc = -1; dc <= W; ++dc) {
|
for (int dc = -1; dc <= W; ++dc) {
|
||||||
s->notifyTilePainted(COL0 + dc, ROW0 - 1);
|
s->notifyTilePainted(COL0 + dc, ROW0 - 1, /*player_id=*/0);
|
||||||
s->notifyTilePainted(COL0 + dc, ROW0 + H);
|
s->notifyTilePainted(COL0 + dc, ROW0 + H, /*player_id=*/0);
|
||||||
}
|
}
|
||||||
for (int dr = 0; dr < H; ++dr) {
|
for (int dr = 0; dr < H; ++dr) {
|
||||||
s->notifyTilePainted(COL0 - 1, ROW0 + dr);
|
s->notifyTilePainted(COL0 - 1, ROW0 + dr, /*player_id=*/0);
|
||||||
s->notifyTilePainted(COL0 + W, ROW0 + dr);
|
s->notifyTilePainted(COL0 + W, ROW0 + dr, /*player_id=*/0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
forceOpen(s1);
|
forceOpen(s1);
|
||||||
|
|||||||
Reference in new issue
Block a user