From 4e9d5df872b72136437442ee4dd518f08b87c5e4 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 24 Apr 2026 14:33:52 +0200 Subject: [PATCH] feat(players): suport del segon jugador amb events amb player_id --- data/maps/pyramid.yaml | 4 +- data/player/morcus.gif | Bin 0 -> 1491 bytes data/player/morcus.yaml | 49 ++++++++++ data/tilesets/tomb.gif | Bin 5051 -> 5202 bytes source/core/system/director.cpp | 11 ++- source/game/debug_config.cpp | 54 ++++++++++- source/game/debug_config.hpp | 14 +++ source/game/entities/concrete/door.cpp | 11 ++- source/game/entities/concrete/floor_trap.cpp | 11 ++- source/game/entities/concrete/mummy.cpp | 11 ++- .../game/entities/concrete/rolling_rock.cpp | 11 ++- source/game/entities/concrete/wall_trap.cpp | 11 ++- source/game/entities/event_bus.hpp | 5 ++ source/game/entities/player.cpp | 16 +++- source/game/entities/player.hpp | 9 +- source/game/map/paint_layer.cpp | 13 ++- source/game/map/paint_layer.hpp | 2 +- source/game/scenes/game_scene.cpp | 85 ++++++++++++++---- source/game/scenes/game_scene.hpp | 9 +- source/game/ui/hud.cpp | 16 ++-- source/utils/defaults.hpp | 16 ++++ 21 files changed, 309 insertions(+), 49 deletions(-) create mode 100644 data/player/morcus.gif create mode 100644 data/player/morcus.yaml diff --git a/data/maps/pyramid.yaml b/data/maps/pyramid.yaml index 28095f6..981b1fd 100644 --- a/data/maps/pyramid.yaml +++ b/data/maps/pyramid.yaml @@ -18,8 +18,8 @@ origin: [-8, 0] # es distingeix a simple vista. Provisional fins que l'art del pintat # estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp # per desactivar el pintat per a aquest jugador. -player1_paint_tile: 2 -player2_paint_tile: 2 +player1_paint_tile: 0 +player2_paint_tile: 4 # Geometria lògica de la piràmide (separada del render visual del tileset). # La graella de sarcòfags, el pitch entre ells i les amplades dels murs diff --git a/data/player/morcus.gif b/data/player/morcus.gif new file mode 100644 index 0000000000000000000000000000000000000000..d4caa57f1a7211044c5224d15ef30ca222df3577 GIT binary patch literal 1491 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 m4dhnqO6Gm$_f5`S)HnS^>p-2jyor|YesR09} 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 a$DQ0`eZ%B!-sb(&%iY}Y&0o?O1OPiL0 StartingPlayers { + if (name == "P1") { return StartingPlayers::P1; } + if (name == "P2") { return StartingPlayers::P2; } + if (name == "BOTH") { return StartingPlayers::BOTH; } + return StartingPlayers::BOTH; + } + + auto isKnownPlayers(const std::string& name) -> bool { + return name == "P1" || name == "P2" || name == "BOTH"; + } + } // namespace auto parseStartScene(const std::string& yaml_content) -> StartScene { @@ -67,6 +78,31 @@ namespace DebugConfig { return parseStartScene(CONTENT); } + auto parseStartingPlayers(const std::string& yaml_content) -> StartingPlayers { + if (yaml_content.empty()) { return StartingPlayers::BOTH; } + try { + auto yaml = fkyaml::node::deserialize(yaml_content); + if (!yaml.contains("starting_players")) { return StartingPlayers::BOTH; } + const auto RAW = yaml["starting_players"].get_value(); + if (!isKnownPlayers(RAW)) { + std::cerr << "debug.yaml: starting_players \"" << RAW << "\" invàlid, arrancant amb BOTH\n"; + return StartingPlayers::BOTH; + } + return playersFromName(RAW); + } catch (const std::exception& e) { + std::cerr << "debug.yaml: error parsejant starting_players: " << e.what() << " — arrancant amb BOTH\n"; + return StartingPlayers::BOTH; + } + } + + auto resolveStartingPlayers(const std::string& file_path) -> StartingPlayers { + std::error_code error; + if (!std::filesystem::exists(file_path, error)) { return StartingPlayers::BOTH; } + const auto CONTENT = readFile(file_path); + if (CONTENT.empty()) { return StartingPlayers::BOTH; } + return parseStartingPlayers(CONTENT); + } + auto toString(StartScene scene) -> const char* { switch (scene) { case StartScene::LOGO: @@ -81,6 +117,18 @@ namespace DebugConfig { return "GAME"; } + auto toString(StartingPlayers players) -> const char* { + switch (players) { + case StartingPlayers::P1: + return "P1"; + case StartingPlayers::P2: + return "P2"; + case StartingPlayers::BOTH: + return "BOTH"; + } + return "BOTH"; + } + void createDefaultIfMissing(const std::string& file_path) { std::error_code error; if (std::filesystem::exists(file_path, error)) { return; } @@ -90,9 +138,11 @@ namespace DebugConfig { return; } file << "# debug.yaml — ajust personal de dev (no versionat).\n"; - file << "# Valors vàlids: LOGO, TITLE, INTRO, GAME.\n"; + file << "# start_scene: LOGO | TITLE | INTRO | GAME\n"; + file << "# starting_players: P1 | P2 | BOTH\n"; file << "start_scene: GAME\n"; - std::cout << "debug.yaml: creat a " << file_path << " amb start_scene=GAME\n"; + file << "starting_players: BOTH\n"; + std::cout << "debug.yaml: creat a " << file_path << " amb start_scene=GAME starting_players=BOTH\n"; } } // namespace DebugConfig diff --git a/source/game/debug_config.hpp b/source/game/debug_config.hpp index 44dad06..182fd21 100644 --- a/source/game/debug_config.hpp +++ b/source/game/debug_config.hpp @@ -14,6 +14,13 @@ namespace DebugConfig { INTRO, GAME }; + // Quins jugadors arrenquen a la GameScene. BOTH és el cas de joc real; + // P1 i P2 són només punts d'entrada de dev per provar cada personatge + // aïllat. Llegit de `debug.yaml` (clau `starting_players`). + enum class StartingPlayers : std::uint8_t { P1, + P2, + BOTH }; + // Parseja un contingut YAML en cru. Si no conté `start_scene`, el valor és // desconegut o el YAML és invàlid, retorna GAME. Pura: no fa I/O. [[nodiscard]] auto parseStartScene(const std::string& yaml_content) -> StartScene; @@ -24,8 +31,15 @@ namespace DebugConfig { // GAME. [[nodiscard]] auto resolveStartScene(const std::string& file_path) -> StartScene; + // Paral·lel a parseStartScene / resolveStartScene per al camp + // `starting_players`. El valor per defecte (tant si el fitxer no + // existeix com si li manca la clau) és BOTH, que és el cas de joc real. + [[nodiscard]] auto parseStartingPlayers(const std::string& yaml_content) -> StartingPlayers; + [[nodiscard]] auto resolveStartingPlayers(const std::string& file_path) -> StartingPlayers; + // Nom canònic de l'enum (útil per a logs): "LOGO" / "TITLE" / "INTRO" / "GAME". [[nodiscard]] auto toString(StartScene scene) -> const char*; + [[nodiscard]] auto toString(StartingPlayers players) -> const char*; // Si el fitxer no existeix, el crea amb `start_scene: GAME` (el valor // per defecte pensat per al dia a dia de dev). Si ja existeix, no el diff --git a/source/game/entities/concrete/door.cpp b/source/game/entities/concrete/door.cpp index 3d172f2..5e0be92 100644 --- a/source/game/entities/concrete/door.cpp +++ b/source/game/entities/concrete/door.cpp @@ -5,6 +5,7 @@ #include "game/entities/entity_factory.hpp" #include "game/entities/event_bus.hpp" +#include "game/entities/player.hpp" namespace { @@ -63,9 +64,15 @@ void Door::update(float delta_time) { } } -void Door::onPlayerContact(Player* /*player*/) { +void Door::onPlayerContact(Player* player) { if (state_ != State::OPEN || level_complete_emitted_) { return; } - if (bus_ != nullptr) { bus_->publish("level_complete"); } + if (bus_ != nullptr) { + bus_->publish(Event{ + .name = "level_complete", + .payload = std::monostate{}, + .player_id = (player != nullptr) ? player->getId() : -1, + }); + } level_complete_emitted_ = true; } diff --git a/source/game/entities/concrete/floor_trap.cpp b/source/game/entities/concrete/floor_trap.cpp index 4457765..fa451f6 100644 --- a/source/game/entities/concrete/floor_trap.cpp +++ b/source/game/entities/concrete/floor_trap.cpp @@ -4,6 +4,7 @@ #include "game/entities/entity_factory.hpp" #include "game/entities/event_bus.hpp" +#include "game/entities/player.hpp" FloorTrap::FloorTrap(const fkyaml::node& yaml) { name_ = yaml.contains("name") ? yaml["name"].get_value() : std::string("floor_trap"); @@ -21,9 +22,15 @@ FloorTrap::FloorTrap(const fkyaml::node& yaml) { } } -void FloorTrap::onPlayerContact(Player* /*player*/) { +void FloorTrap::onPlayerContact(Player* player) { if (!isActive()) { return; } - if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); } + if (bus_ != nullptr) { + bus_->publish(Event{ + .name = "player_hit", + .payload = getDamage(), + .player_id = (player != nullptr) ? player->getId() : -1, + }); + } } REGISTER_ENTITY("floor_trap", FloorTrap) diff --git a/source/game/entities/concrete/mummy.cpp b/source/game/entities/concrete/mummy.cpp index 63f83c0..9128738 100644 --- a/source/game/entities/concrete/mummy.cpp +++ b/source/game/entities/concrete/mummy.cpp @@ -9,6 +9,7 @@ #include "game/entities/entity_factory.hpp" #include "game/entities/event_bus.hpp" +#include "game/entities/player.hpp" #include "game/map/map.hpp" namespace { @@ -294,8 +295,14 @@ void Mummy::applyChoice(Direction dir) { } } -void Mummy::onPlayerContact(Player* /*player*/) { - if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); } +void Mummy::onPlayerContact(Player* player) { + if (bus_ != nullptr) { + bus_->publish(Event{ + .name = "player_hit", + .payload = getDamage(), + .player_id = (player != nullptr) ? player->getId() : -1, + }); + } } REGISTER_ENTITY("mummy", Mummy) diff --git a/source/game/entities/concrete/rolling_rock.cpp b/source/game/entities/concrete/rolling_rock.cpp index 212df64..e40f7d4 100644 --- a/source/game/entities/concrete/rolling_rock.cpp +++ b/source/game/entities/concrete/rolling_rock.cpp @@ -4,6 +4,7 @@ #include "game/entities/entity_factory.hpp" #include "game/entities/event_bus.hpp" +#include "game/entities/player.hpp" #include "game/map/map.hpp" namespace { @@ -60,8 +61,14 @@ void RollingRock::step(float delta_time) { setDirection(0.0F, 0.0F); } -void RollingRock::onPlayerContact(Player* /*player*/) { - if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); } +void RollingRock::onPlayerContact(Player* player) { + if (bus_ != nullptr) { + bus_->publish(Event{ + .name = "player_hit", + .payload = getDamage(), + .player_id = (player != nullptr) ? player->getId() : -1, + }); + } } REGISTER_ENTITY("rolling_rock", RollingRock) diff --git a/source/game/entities/concrete/wall_trap.cpp b/source/game/entities/concrete/wall_trap.cpp index 43345d7..5b385f4 100644 --- a/source/game/entities/concrete/wall_trap.cpp +++ b/source/game/entities/concrete/wall_trap.cpp @@ -4,6 +4,7 @@ #include "game/entities/entity_factory.hpp" #include "game/entities/event_bus.hpp" +#include "game/entities/player.hpp" WallTrap::WallTrap(const fkyaml::node& yaml) { name_ = yaml.contains("name") ? yaml["name"].get_value() : std::string("wall_trap"); @@ -21,9 +22,15 @@ WallTrap::WallTrap(const fkyaml::node& yaml) { } } -void WallTrap::onPlayerContact(Player* /*player*/) { +void WallTrap::onPlayerContact(Player* player) { if (!isActive()) { return; } - if (bus_ != nullptr) { bus_->publish("player_hit", getDamage()); } + if (bus_ != nullptr) { + bus_->publish(Event{ + .name = "player_hit", + .payload = getDamage(), + .player_id = (player != nullptr) ? player->getId() : -1, + }); + } } REGISTER_ENTITY("wall_trap", WallTrap) diff --git a/source/game/entities/event_bus.hpp b/source/game/entities/event_bus.hpp index a61a1cf..fd7789e 100644 --- a/source/game/entities/event_bus.hpp +++ b/source/game/entities/event_bus.hpp @@ -45,6 +45,11 @@ struct Event { std::string name; 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 + // entitat). Usat per al HUD (dispatch a P1/P2) i per a regles futures + // de puntuació que atribueixin accions al jugador concret. + int player_id{-1}; }; class EventBus { diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 1c75692..db7ce2c 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -222,13 +222,19 @@ void Player::updateFacingAndTurn(float delta_time, float vx, float vy) { } } -Player::Player(const std::string& anim_key, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index) +Player::Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index) : AnimatedSprite(Resource::Cache::get().getAnimationData(anim_key)), + id_(id), controls_(controls), map_(map_info), paint_tile_index_(paint_tile_index) { setPos(x, y); - setCurrentAnimation("stand"); + // Postura inicial mirant cap al jugador (cara avall), no de costat: + // forcem facing_ a DOWN i l'animació `stand_down` abans del primer + // frame d'input — el primer `update` la preservarà si l'usuari no + // prem cap tecla. + facing_ = Facing::DOWN; + setCurrentAnimation("stand_down"); } void Player::update(float delta_time) { @@ -308,7 +314,11 @@ void Player::update(float delta_time) { if (COL != last_tile_col_ || ROW != last_tile_row_) { last_tile_col_ = COL; last_tile_row_ = ROW; - bus_->publish("on_tile_entered", PaintedTile{.col = COL, .row = ROW, .tile_index = paint_tile_index_}); + bus_->publish(Event{ + .name = "on_tile_entered", + .payload = PaintedTile{.col = COL, .row = ROW, .tile_index = paint_tile_index_}, + .player_id = id_, + }); } } } diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 89ff0d7..7109eb7 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -42,10 +42,16 @@ class Player : public AnimatedSprite { // Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor. static void loadFrom(const std::string& yaml_path); - Player(const std::string& anim_key, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1); + Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1); void update(float delta_time) override; + // Identificador local (0 = P1, 1 = P2…). S'usa als events del bus + // perquè els consumidors (HUD, futures regles de puntuació) sàpiguen + // quin jugador ha originat l'event. No té cap vinculació amb la + // xarxa — és únic per dins del procés. + [[nodiscard]] auto getId() const -> int { return id_; } + // Y de la base del jugador (cara inferior de la caixa de colisió). Usada // per al Z-order contra els sarcs: peus per sobre d'un sarc → darrere; // per sota → davant. @@ -104,6 +110,7 @@ class Player : public AnimatedSprite { float (Player::*get_pos)() const, void (Player::*set_pos)(float)); + int id_{-1}; Controls controls_; // Còpia plana de la geometria del mapa + punters a `collides`/ // `inside_playfield`. POD trivial; copiada al ctor i no muta durant diff --git a/source/game/map/paint_layer.cpp b/source/game/map/paint_layer.cpp index 35be755..297b629 100644 --- a/source/game/map/paint_layer.cpp +++ b/source/game/map/paint_layer.cpp @@ -60,8 +60,11 @@ PaintLayer::PaintLayer(const Tilemap& tilemap, EventBus* bus) // (sòl transitable) via `Map::isPaintable` i, si aplica, marquem // i re-publiquem `tile_painted` amb només la posició. on_tile_sub_ = bus_->subscribe("on_tile_entered", [this](const Event& ev) { + // Re-propaguem el `player_id` de qui ha trepitjat la cel·la, perquè + // `tile_painted` (que consumeixen els sarcòfags i el HUD futur) + // sàpiga quin jugador ha pintat. const auto* paint = std::get_if(&ev.payload); - if (paint != nullptr) { tryPaint(paint->col, paint->row, paint->tile_index); } + if (paint != nullptr) { tryPaint(paint->col, paint->row, paint->tile_index, ev.player_id); } }); } @@ -79,7 +82,7 @@ auto PaintLayer::isPainted(int col, int row) const -> bool { return painted_[static_cast(row)][static_cast(col)] >= 0; } -auto PaintLayer::tryPaint(int col, int row, int tile_index) -> bool { +auto PaintLayer::tryPaint(int col, int row, int tile_index, int player_id) -> bool { if (col < 0 || col >= cols_ || row < 0 || row >= rows_) { return false; } if (tile_index < 0) { return false; } if (painted_[static_cast(row)][static_cast(col)] >= 0) { @@ -92,7 +95,11 @@ auto PaintLayer::tryPaint(int col, int row, int tile_index) -> bool { blitPaintTile(col, row, tile_index); if (bus_ != nullptr) { - bus_->publish("tile_painted", TilePos{.col = col, .row = row}); + bus_->publish(Event{ + .name = "tile_painted", + .payload = TilePos{.col = col, .row = row}, + .player_id = player_id, + }); } return true; } diff --git a/source/game/map/paint_layer.hpp b/source/game/map/paint_layer.hpp index 4088ef8..d67c09e 100644 --- a/source/game/map/paint_layer.hpp +++ b/source/game/map/paint_layer.hpp @@ -44,7 +44,7 @@ class PaintLayer { // Intent de pintat directe (exposat per a tests i actors que pinten // sense passar pel bus). Retorna true si el tile és pintable i no // estava ja pintat; en aquest cas, publica `tile_painted`. - auto tryPaint(int col, int row, int tile_index) -> bool; + auto tryPaint(int col, int row, int tile_index, int player_id = -1) -> bool; [[nodiscard]] auto isPainted(int col, int row) const -> bool; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 34009fa..7de426f 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -69,7 +69,10 @@ auto GameScene::defaultConfig() -> Config { .map_yaml = Defaults::Pyramid::MAP_YAML, .player_sprite = Defaults::Pyramid::PLAYER_SPRITE, .player_yaml = Defaults::Pyramid::PLAYER_YAML, + .player2_sprite = Defaults::Pyramid::PLAYER_SPRITE_P2, + .player2_yaml = Defaults::Pyramid::PLAYER_YAML_P2, .music_track = Defaults::Pyramid::MUSIC_TRACK, + .starting_players = DebugConfig::StartingPlayers::BOTH, }; } @@ -78,15 +81,32 @@ GameScene::GameScene(Config config) tilemap_(makeTilemap(config_.map_yaml)) { // Convenció: el Resource::Cache indexa per basename del fitxer, perquè // la mateixa clau l'usa el parseador d'animació (YAML > tileSetFile). - const auto SPRITE_KEY = std::filesystem::path(config_.player_sprite).filename().string(); - const auto ANIM_KEY = std::filesystem::path(config_.player_yaml).filename().string(); - Resource::Cache::get().loadSurface(SPRITE_KEY, config_.player_sprite); - Resource::Cache::get().loadAnimation(ANIM_KEY, config_.player_yaml); + // Lambda perquè tenim dos conjunts d'assets (sam/morcus) i l'alta al + // cache és idèntica per als dos; la clau d'animació ens la quedem per + // passar-la al ctor del Player corresponent. + auto load_player_assets = [](const std::string& sprite, const std::string& yaml) -> std::string { + const auto SPRITE_KEY = std::filesystem::path(sprite).filename().string(); + const auto ANIM_KEY = std::filesystem::path(yaml).filename().string(); + Resource::Cache::get().loadSurface(SPRITE_KEY, sprite); + Resource::Cache::get().loadAnimation(ANIM_KEY, yaml); + return ANIM_KEY; + }; + + const bool USE_P1 = (config_.starting_players == DebugConfig::StartingPlayers::P1 || config_.starting_players == DebugConfig::StartingPlayers::BOTH); + const bool USE_P2 = (config_.starting_players == DebugConfig::StartingPlayers::P2 || config_.starting_players == DebugConfig::StartingPlayers::BOTH); + + std::string anim_p1; + std::string anim_p2; + if (USE_P1) { anim_p1 = load_player_assets(config_.player_sprite, config_.player_yaml); } + if (USE_P2) { anim_p2 = load_player_assets(config_.player2_sprite, config_.player2_yaml); } // Geometria de col·lisió del jugador (caixa 16×16, base 12): carregada // del mateix YAML que l'animació. Ha d'estar feta abans d'instanciar - // Player perquè el spawn consulta `collision_*`. - Player::loadFrom(config_.player_yaml); + // Player perquè el spawn consulta `collision_*`. Els membres de Player + // són `static inline`, compartits per tots els jugadors: cridem + // `loadFrom` amb el YAML del primer jugador actiu (P1 o P2). Els dos + // personatges usen la mateixa geometria (morcus és un repaletat de sam). + Player::loadFrom(USE_P1 ? config_.player_yaml : config_.player2_yaml); // Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia, // etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa @@ -97,23 +117,30 @@ GameScene::GameScene(Config config) // Subscripcions bàsiques al bus del nivell: de moment només logs, sense // tocar ni la vida del jugador ni l'estat de la escena. Serà la base // sobre la qual cablejar HUD i fi de nivell més endavant. - entity_manager_.getBus().subscribe("level_complete", [](const Event& /*ev*/) { - std::cout << "[LEVEL COMPLETE]" << '\n'; + entity_manager_.getBus().subscribe("level_complete", [](const Event& ev) { + std::cout << "[LEVEL COMPLETE] player_id=" << ev.player_id << '\n'; }); entity_manager_.getBus().subscribe("player_hit", [](const Event& ev) { const auto* dmg = std::get_if(&ev.payload); - std::cout << "[PLAYER HIT] dmg=" << (dmg != nullptr ? *dmg : 0) << '\n'; + std::cout << "[PLAYER HIT] player_id=" << ev.player_id + << " dmg=" << (dmg != nullptr ? *dmg : 0) << '\n'; }); - // Jugador 0: fletxes del teclat (Defaults::KeysGame). Instanciar-ne un - // segon només requereix afegir una altra entrada amb uns Controls - // distints (p.ex. WASD). + // 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. const Player::Controls CONTROLS_P1{ .up = Defaults::KeysGame::UP, .down = Defaults::KeysGame::DOWN, .left = Defaults::KeysGame::LEFT, .right = Defaults::KeysGame::RIGHT, }; + const Player::Controls CONTROLS_P2{ + .up = Defaults::KeysGameP2::UP, + .down = Defaults::KeysGameP2::DOWN, + .left = Defaults::KeysGameP2::LEFT, + .right = Defaults::KeysGameP2::RIGHT, + }; // Spawn centrat al passadís exterior superior: la caixa de col·lisió // queda dins del passadís vertical del mig (entre columnes 1 i 2) i // centrada en Y al passadís exterior superior. Així no naix dins de @@ -123,11 +150,31 @@ GameScene::GameScene(Config config) static_cast(Map::playfield_y) + (static_cast(Map::outer_corridor) / 2.0F); const auto SPAWN_X = CORRIDOR_CENTER_X - (static_cast(Player::sprite_w) / 2.0F); const auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast(Player::collision_y_offset) - (static_cast(Player::collision_size) / 2.0F); + + // En mode BOTH separem els jugadors un tile a banda i banda del + // centre perquè no naixen superposats. En mode single-player + // (P1 o P2 aïllats per a proves de dev) l'únic jugador arrenca al + // centre exacte. + const bool BOTH = (USE_P1 && USE_P2); + const auto P1_SPAWN_X = BOTH ? SPAWN_X - static_cast(Map::tile_size) : SPAWN_X; + const auto P2_SPAWN_X = BOTH ? SPAWN_X + static_cast(Map::tile_size) : SPAWN_X; + // Índex de tile amb què pinta cada jugador — llegit del YAML del mapa // (camps opcionals `player1_paint_tile` / `player2_paint_tile`). Si no // hi són, `paint_tile_index_ = -1` i el Player no deixa rastre. - const int P1_PAINT = readOptionalInt(config_.map_yaml, "player1_paint_tile", -1); - players_.emplace_back(std::make_unique(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1, Map::makeInfo(), P1_PAINT)); + // Ids canònics dels jugadors: 0 = P1, 1 = P2. Els events del bus els + // porten per permetre dispatch correcte al HUD i a futures regles de + // puntuació (veure `Event::player_id`). + constexpr int ID_P1 = 0; + constexpr int ID_P2 = 1; + if (USE_P1) { + const int P1_PAINT = readOptionalInt(config_.map_yaml, "player1_paint_tile", -1); + players_.emplace_back(std::make_unique(anim_p1, ID_P1, P1_SPAWN_X, SPAWN_Y, CONTROLS_P1, Map::makeInfo(), P1_PAINT)); + } + if (USE_P2) { + const int P2_PAINT = readOptionalInt(config_.map_yaml, "player2_paint_tile", -1); + players_.emplace_back(std::make_unique(anim_p2, ID_P2, P2_SPAWN_X, SPAWN_Y, CONTROLS_P2, Map::makeInfo(), P2_PAINT)); + } // PaintLayer: font única del pintat del nivell. Es crea després de // `loadFromLevel` perquè l'EntityManager ja ha de tenir els sarcòfags @@ -137,10 +184,10 @@ GameScene::GameScene(Config config) // manager per subscriure's a `on_tile_entered` i publicar `tile_painted`. paint_layer_ = std::make_unique(tilemap_, &entity_manager_.getBus()); - // Connectem el Player al bus perquè emeti `on_tile_entered` i el - // PaintLayer (subscrit ja) processe el pintat; el fan-out cap als + // Connectem tots els jugadors al bus perquè emeten `on_tile_entered`; + // el PaintLayer (subscrit ja) processa el pintat i el fan-out cap als // sarcòfags passa per `tile_painted`. - players_.back()->setEventBus(&entity_manager_.getBus()); + for (auto& p : players_) { p->setEventBus(&entity_manager_.getBus()); } // Marcador (HUD) de la franja superior. Reutilitza la font 8bithud i // la paleta "ui" ja precarregades pel Director — no toca disc des @@ -151,6 +198,10 @@ GameScene::GameScene(Config config) auto hud_font = std::make_shared(hud_font_surface, UiConfig::get().hudFont().fnt); hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning); hud_ = std::make_unique(entity_manager_.getBus(), hud_font); + // Si hi ha P2 actiu, activem el seu bloc (per defecte el HUD naix amb + // només P1). Mentre els events del bus no porten `player_id`, els hits + // segueixen aplicant-se a P1 (veure TODO a Hud::onPlayerHit). + if (USE_P2) { hud_->activatePlayer(2); } // Música de fons en bucle infinit. El track ja està al Resource::Cache // (preload al Director), així que playMusic només mou el punter cap a diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index ccabfbc..6e4bf0a 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -7,6 +7,7 @@ #include "core/scene/scene.hpp" #include "core/ui/ui_config.hpp" +#include "game/debug_config.hpp" #include "game/entities/entity_manager.hpp" #include "game/map/paint_layer.hpp" #include "game/map/tilemap.hpp" @@ -22,13 +23,17 @@ class Text; class GameScene : public Scene { public: // Conjunt de rutes que defineixen una piràmide concreta (mapa + tilemap - // + sprites del jugador + música). Cada piràmide serà un Config distint; - // `defaultConfig()` retorna la primera (Defaults::Pyramid). + // + sprites dels jugadors + música). Cada piràmide serà un Config distint; + // `defaultConfig()` retorna la primera (Defaults::Pyramid). `starting_players` + // controla quins jugadors instancia el ctor: només P1, només P2 o els dos. struct Config { std::string map_yaml; std::string player_sprite; std::string player_yaml; + std::string player2_sprite; + std::string player2_yaml; std::string music_track; + DebugConfig::StartingPlayers starting_players{DebugConfig::StartingPlayers::BOTH}; }; [[nodiscard]] static auto defaultConfig() -> Config; diff --git a/source/game/ui/hud.cpp b/source/game/ui/hud.cpp index 1f1cb02..6c402c3 100644 --- a/source/game/ui/hud.cpp +++ b/source/game/ui/hud.cpp @@ -114,16 +114,20 @@ Hud::Hud(EventBus& bus, std::shared_ptr font) // i puntuacions, ampliarem aquí sense tocar callers. subs_.push_back(bus_.subscribe("player_hit", [this](const Event& ev) { const auto* dmg = std::get_if(&ev.payload); - // TODO: quan l'event porte player_id, fer dispatch al bloc real. - // Mentrestant, apliquem el hit a P1 (consistent amb el log del - // GameScene actual, que tampoc desambigua el jugador). - constexpr std::size_t IDX = 0; + // Dispatch pel `player_id` de l'event. 0 = P1 (bloc LEFT), 1 = P2 + // (bloc RIGHT). Si l'event arriba sense id (-1, event world-wide) + // fem fallback a P1 per no perdre'l — a la pràctica tots els + // publishers de `player_hit` actuals sí que porten id. + const std::size_t IDX = (ev.player_id == 1) ? 1U : 0U; + if (!players_[IDX].active) { return; } if (players_[IDX].lives > 0) { --players_[IDX].lives; } - auto& blk = blocks_[IDX]; // P1 → bloc esquerre + const std::size_t BLK_IDX = (IDX == 0) ? static_cast(BlockId::LEFT) + : static_cast(BlockId::RIGHT); + auto& blk = blocks_[BLK_IDX]; blk.flash_ms_left = UiConfig::get().hudFlashMs(); blk.shake_ms_left = UiConfig::get().hudShakeMs(); blk.shake_intensity = 1.5F; - std::cout << "[HUD] player_hit dmg=" << (dmg != nullptr ? *dmg : 0) + std::cout << "[HUD] player_hit p" << (IDX + 1) << " dmg=" << (dmg != nullptr ? *dmg : 0) << " lives=" << players_[IDX].lives << '\n'; })); diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 0f49c0e..77f6a72 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -80,6 +80,17 @@ namespace Defaults { constexpr SDL_Scancode EXIT = SDL_SCANCODE_ESCAPE; } // namespace KeysGame + // Tecles del segon jugador local (WASD). Simètriques a KeysGame però + // mapejades a la mà esquerra del teclat. Cada Player instancia amb els + // seus propis Controls, així que aquests són un recull de fàbrica: + // canviar-los ací canvia el mapatge per a qualsevol escena que els use. + namespace KeysGameP2 { + constexpr SDL_Scancode UP = SDL_SCANCODE_W; + constexpr SDL_Scancode DOWN = SDL_SCANCODE_S; + constexpr SDL_Scancode LEFT = SDL_SCANCODE_A; + constexpr SDL_Scancode RIGHT = SDL_SCANCODE_D; + } // namespace KeysGameP2 + namespace Player { constexpr float SPEED_PX_PER_S = 60.0F; // Velocitat base del jugador (sense sprint / slow-zones). } // namespace Player @@ -209,6 +220,11 @@ namespace Defaults { constexpr const char* MAP_YAML = "data/maps/pyramid.yaml"; constexpr const char* PLAYER_SPRITE = "data/player/sam.gif"; constexpr const char* PLAYER_YAML = "data/player/sam.yaml"; + // Personatge del segon jugador. Clonat de sam amb repaletat (roig→blau); + // la geometria del sprite i les animacions coincideixen amb les de P1, + // així que Player::loadFrom només cal cridar-la amb un dels dos YAMLs. + constexpr const char* PLAYER_SPRITE_P2 = "data/player/morcus.gif"; + constexpr const char* PLAYER_YAML_P2 = "data/player/morcus.yaml"; constexpr const char* MUSIC_TRACK = "pyramid_1.ogg"; } // namespace Pyramid