feat(players): suport del segon jugador amb events amb player_id
This commit is contained in:
1 parent
74dad71cc2
commit
4e9d5df872
21 files changed
+308
-48
No files matched your search
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -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)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.1 KiB |
@@ -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<Scene> { return std::make_unique<GameScene>(); };
|
||||
auto make_game = [STARTING_PLAYERS]() -> std::unique_ptr<Scene> {
|
||||
auto cfg = GameScene::defaultConfig();
|
||||
cfg.starting_players = STARTING_PLAYERS;
|
||||
return std::make_unique<GameScene>(std::move(cfg));
|
||||
};
|
||||
auto make_intro = [placeholder_text, make_game]() -> std::unique_ptr<Scene> {
|
||||
return std::make_unique<IntroScene>(placeholder_text, make_game);
|
||||
};
|
||||
|
||||
@@ -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<std::string>();
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>() : 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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>() : 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)
|
||||
@@ -45,6 +45,11 @@ struct Event {
|
||||
std::string name;
|
||||
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile>;
|
||||
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 {
|
||||
|
||||
@@ -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_,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<PaintedTile>(&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<std::size_t>(row)][static_cast<std::size_t>(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<std::size_t>(row)][static_cast<std::size_t>(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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<int>(&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<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F);
|
||||
const auto SPAWN_X = CORRIDOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F);
|
||||
const auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(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<float>(Map::tile_size) : SPAWN_X;
|
||||
const auto P2_SPAWN_X = BOTH ? SPAWN_X + static_cast<float>(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.
|
||||
// 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<Player>(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
||||
players_.emplace_back(std::make_unique<Player>(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<Player>(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<PaintLayer>(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<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
|
||||
hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
|
||||
hud_ = std::make_unique<Hud>(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
|
||||
|
||||
@@ -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;
|
||||
|
||||
+10
-6
@@ -114,16 +114,20 @@ Hud::Hud(EventBus& bus, std::shared_ptr<Text> 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<int>(&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<std::size_t>(BlockId::LEFT)
|
||||
: static_cast<std::size_t>(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';
|
||||
}));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user