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 0000000..d4caa57 Binary files /dev/null and b/data/player/morcus.gif differ diff --git a/data/player/morcus.yaml b/data/player/morcus.yaml new file mode 100644 index 0000000..5146dbc --- /dev/null +++ b/data/player/morcus.yaml @@ -0,0 +1,49 @@ +# player animation +tileSetFile: morcus.gif +frameWidth: 16 +frameHeight: 32 + +# Geometria de col·lisió del jugador (independent de l'animació): +# size → costat del quadrat de col·lisió (16×16) +# base_size → costat del quadrat visual inferior del sprite (els "peus"); +# s'usa per centrar la caixa de col·lisió sobre ell. +collision: + size: 16 + base_size: 16 + +animations: + - name: stand_down + frames: [0] + + - name: stand_up + frames: [4] + + - name: stand + frames: [8] + + - name: walk_down + speed: 0.1 + loopFrom: 0 + frames: [0, 1, 2, 3] + + - name: walk_up + speed: 0.1 + loopFrom: 0 + frames: [4, 5, 6, 7] + + - name: walk + speed: 0.1 + loopFrom: 0 + frames: [8, 9, 10, 11] + +# Gir de 180°: mentre dura, el facing queda fixat a la direcció +# intermèdia (efecte purament visual; el moviment continua sense +# alterar-se). Els girs de 90° no passen per cap intermedi. +turn: + duration_ms: 80 + # Direcció intermèdia per a cada gir de 180° (sentit horari). + transitions: + west_to_east: down # passa per baix (stand_down) + east_to_west: up # passa per dalt (stand_up) + north_to_south: east # passa per la dreta (stand, sense flip) + south_to_north: west # passa per l'esquerra (stand, amb flip) diff --git a/data/tilesets/tomb.gif b/data/tilesets/tomb.gif index dbf1dc0..2c4761d 100644 Binary files a/data/tilesets/tomb.gif and b/data/tilesets/tomb.gif differ diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index ff36c5f..a924202 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -176,12 +176,19 @@ Director::Director() { const auto DEBUG_YAML = system_folder_ + "debug.yaml"; DebugConfig::createDefaultIfMissing(DEBUG_YAML); const auto START = DebugConfig::resolveStartScene(DEBUG_YAML); + const auto STARTING_PLAYERS = DebugConfig::resolveStartingPlayers(DEBUG_YAML); #else const auto START = DebugConfig::StartScene::LOGO; + const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH; #endif - std::cout << "Director: start_scene=" << DebugConfig::toString(START) << '\n'; + std::cout << "Director: start_scene=" << DebugConfig::toString(START) + << " starting_players=" << DebugConfig::toString(STARTING_PLAYERS) << '\n'; - auto make_game = []() -> std::unique_ptr { return std::make_unique(); }; + auto make_game = [STARTING_PLAYERS]() -> std::unique_ptr { + auto cfg = GameScene::defaultConfig(); + cfg.starting_players = STARTING_PLAYERS; + return std::make_unique(std::move(cfg)); + }; auto make_intro = [placeholder_text, make_game]() -> std::unique_ptr { return std::make_unique(placeholder_text, make_game); }; diff --git a/source/game/debug_config.cpp b/source/game/debug_config.cpp index 6070e59..7ff9291 100644 --- a/source/game/debug_config.cpp +++ b/source/game/debug_config.cpp @@ -33,6 +33,17 @@ namespace DebugConfig { return name == "LOGO" || name == "TITLE" || name == "INTRO" || name == "GAME"; } + auto playersFromName(const std::string& name) -> 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