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
|
# 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
|
# estigui definit; canviar aquí l'índex és tot el que cal. Omet el camp
|
||||||
# per desactivar el pintat per a aquest jugador.
|
# per desactivar el pintat per a aquest jugador.
|
||||||
player1_paint_tile: 2
|
player1_paint_tile: 0
|
||||||
player2_paint_tile: 2
|
player2_paint_tile: 4
|
||||||
|
|
||||||
# Geometria lògica de la piràmide (separada del render visual del tileset).
|
# 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
|
# 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";
|
const auto DEBUG_YAML = system_folder_ + "debug.yaml";
|
||||||
DebugConfig::createDefaultIfMissing(DEBUG_YAML);
|
DebugConfig::createDefaultIfMissing(DEBUG_YAML);
|
||||||
const auto START = DebugConfig::resolveStartScene(DEBUG_YAML);
|
const auto START = DebugConfig::resolveStartScene(DEBUG_YAML);
|
||||||
|
const auto STARTING_PLAYERS = DebugConfig::resolveStartingPlayers(DEBUG_YAML);
|
||||||
#else
|
#else
|
||||||
const auto START = DebugConfig::StartScene::LOGO;
|
const auto START = DebugConfig::StartScene::LOGO;
|
||||||
|
const auto STARTING_PLAYERS = DebugConfig::StartingPlayers::BOTH;
|
||||||
#endif
|
#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> {
|
auto make_intro = [placeholder_text, make_game]() -> std::unique_ptr<Scene> {
|
||||||
return std::make_unique<IntroScene>(placeholder_text, make_game);
|
return std::make_unique<IntroScene>(placeholder_text, make_game);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ namespace DebugConfig {
|
|||||||
return name == "LOGO" || name == "TITLE" || name == "INTRO" || name == "GAME";
|
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
|
} // namespace
|
||||||
|
|
||||||
auto parseStartScene(const std::string& yaml_content) -> StartScene {
|
auto parseStartScene(const std::string& yaml_content) -> StartScene {
|
||||||
@@ -67,6 +78,31 @@ namespace DebugConfig {
|
|||||||
return parseStartScene(CONTENT);
|
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* {
|
auto toString(StartScene scene) -> const char* {
|
||||||
switch (scene) {
|
switch (scene) {
|
||||||
case StartScene::LOGO:
|
case StartScene::LOGO:
|
||||||
@@ -81,6 +117,18 @@ namespace DebugConfig {
|
|||||||
return "GAME";
|
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) {
|
void createDefaultIfMissing(const std::string& file_path) {
|
||||||
std::error_code error;
|
std::error_code error;
|
||||||
if (std::filesystem::exists(file_path, error)) { return; }
|
if (std::filesystem::exists(file_path, error)) { return; }
|
||||||
@@ -90,9 +138,11 @@ namespace DebugConfig {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
file << "# debug.yaml — ajust personal de dev (no versionat).\n";
|
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";
|
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
|
} // namespace DebugConfig
|
||||||
@@ -14,6 +14,13 @@ namespace DebugConfig {
|
|||||||
INTRO,
|
INTRO,
|
||||||
GAME };
|
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
|
// 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.
|
// desconegut o el YAML és invàlid, retorna GAME. Pura: no fa I/O.
|
||||||
[[nodiscard]] auto parseStartScene(const std::string& yaml_content) -> StartScene;
|
[[nodiscard]] auto parseStartScene(const std::string& yaml_content) -> StartScene;
|
||||||
@@ -24,8 +31,15 @@ namespace DebugConfig {
|
|||||||
// GAME.
|
// GAME.
|
||||||
[[nodiscard]] auto resolveStartScene(const std::string& file_path) -> StartScene;
|
[[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".
|
// Nom canònic de l'enum (útil per a logs): "LOGO" / "TITLE" / "INTRO" / "GAME".
|
||||||
[[nodiscard]] auto toString(StartScene scene) -> const char*;
|
[[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
|
// 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
|
// 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/entity_factory.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
|
|
||||||
namespace {
|
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 (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;
|
level_complete_emitted_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
|
|
||||||
FloorTrap::FloorTrap(const fkyaml::node& yaml) {
|
FloorTrap::FloorTrap(const fkyaml::node& yaml) {
|
||||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("floor_trap");
|
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 (!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)
|
REGISTER_ENTITY("floor_trap", FloorTrap)
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -294,8 +295,14 @@ void Mummy::applyChoice(Direction dir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mummy::onPlayerContact(Player* /*player*/) {
|
void Mummy::onPlayerContact(Player* player) {
|
||||||
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("mummy", Mummy)
|
REGISTER_ENTITY("mummy", Mummy)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -60,8 +61,14 @@ void RollingRock::step(float delta_time) {
|
|||||||
setDirection(0.0F, 0.0F);
|
setDirection(0.0F, 0.0F);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RollingRock::onPlayerContact(Player* /*player*/) {
|
void RollingRock::onPlayerContact(Player* player) {
|
||||||
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("rolling_rock", RollingRock)
|
REGISTER_ENTITY("rolling_rock", RollingRock)
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
|
|
||||||
WallTrap::WallTrap(const fkyaml::node& yaml) {
|
WallTrap::WallTrap(const fkyaml::node& yaml) {
|
||||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("wall_trap");
|
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 (!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)
|
REGISTER_ENTITY("wall_trap", WallTrap)
|
||||||
@@ -45,6 +45,11 @@ struct Event {
|
|||||||
std::string name;
|
std::string name;
|
||||||
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile>;
|
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile>;
|
||||||
Payload payload;
|
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 {
|
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)),
|
: AnimatedSprite(Resource::Cache::get().getAnimationData(anim_key)),
|
||||||
|
id_(id),
|
||||||
controls_(controls),
|
controls_(controls),
|
||||||
map_(map_info),
|
map_(map_info),
|
||||||
paint_tile_index_(paint_tile_index) {
|
paint_tile_index_(paint_tile_index) {
|
||||||
setPos(x, y);
|
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) {
|
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_) {
|
if (COL != last_tile_col_ || ROW != last_tile_row_) {
|
||||||
last_tile_col_ = COL;
|
last_tile_col_ = COL;
|
||||||
last_tile_row_ = ROW;
|
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.
|
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
|
||||||
static void loadFrom(const std::string& yaml_path);
|
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;
|
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
|
// 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 al Z-order contra els sarcs: peus per sobre d'un sarc → darrere;
|
||||||
// per sota → davant.
|
// per sota → davant.
|
||||||
@@ -104,6 +110,7 @@ class Player : public AnimatedSprite {
|
|||||||
float (Player::*get_pos)() const,
|
float (Player::*get_pos)() const,
|
||||||
void (Player::*set_pos)(float));
|
void (Player::*set_pos)(float));
|
||||||
|
|
||||||
|
int id_{-1};
|
||||||
Controls controls_;
|
Controls controls_;
|
||||||
// Còpia plana de la geometria del mapa + punters a `collides`/
|
// Còpia plana de la geometria del mapa + punters a `collides`/
|
||||||
// `inside_playfield`. POD trivial; copiada al ctor i no muta durant
|
// `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
|
// (sòl transitable) via `Map::isPaintable` i, si aplica, marquem
|
||||||
// i re-publiquem `tile_painted` amb només la posició.
|
// i re-publiquem `tile_painted` amb només la posició.
|
||||||
on_tile_sub_ = bus_->subscribe("on_tile_entered", [this](const Event& ev) {
|
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);
|
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;
|
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 (col < 0 || col >= cols_ || row < 0 || row >= rows_) { return false; }
|
||||||
if (tile_index < 0) { return false; }
|
if (tile_index < 0) { return false; }
|
||||||
if (painted_[static_cast<std::size_t>(row)][static_cast<std::size_t>(col)] >= 0) {
|
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);
|
blitPaintTile(col, row, tile_index);
|
||||||
|
|
||||||
if (bus_ != nullptr) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class PaintLayer {
|
|||||||
// Intent de pintat directe (exposat per a tests i actors que pinten
|
// 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
|
// sense passar pel bus). Retorna true si el tile és pintable i no
|
||||||
// estava ja pintat; en aquest cas, publica `tile_painted`.
|
// 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;
|
[[nodiscard]] auto isPainted(int col, int row) const -> bool;
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ auto GameScene::defaultConfig() -> Config {
|
|||||||
.map_yaml = Defaults::Pyramid::MAP_YAML,
|
.map_yaml = Defaults::Pyramid::MAP_YAML,
|
||||||
.player_sprite = Defaults::Pyramid::PLAYER_SPRITE,
|
.player_sprite = Defaults::Pyramid::PLAYER_SPRITE,
|
||||||
.player_yaml = Defaults::Pyramid::PLAYER_YAML,
|
.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,
|
.music_track = Defaults::Pyramid::MUSIC_TRACK,
|
||||||
|
.starting_players = DebugConfig::StartingPlayers::BOTH,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,15 +81,32 @@ GameScene::GameScene(Config config)
|
|||||||
tilemap_(makeTilemap(config_.map_yaml)) {
|
tilemap_(makeTilemap(config_.map_yaml)) {
|
||||||
// Convenció: el Resource::Cache indexa per basename del fitxer, perquè
|
// Convenció: el Resource::Cache indexa per basename del fitxer, perquè
|
||||||
// la mateixa clau l'usa el parseador d'animació (YAML > tileSetFile).
|
// la mateixa clau l'usa el parseador d'animació (YAML > tileSetFile).
|
||||||
const auto SPRITE_KEY = std::filesystem::path(config_.player_sprite).filename().string();
|
// Lambda perquè tenim dos conjunts d'assets (sam/morcus) i l'alta al
|
||||||
const auto ANIM_KEY = std::filesystem::path(config_.player_yaml).filename().string();
|
// cache és idèntica per als dos; la clau d'animació ens la quedem per
|
||||||
Resource::Cache::get().loadSurface(SPRITE_KEY, config_.player_sprite);
|
// passar-la al ctor del Player corresponent.
|
||||||
Resource::Cache::get().loadAnimation(ANIM_KEY, config_.player_yaml);
|
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
|
// 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
|
// del mateix YAML que l'animació. Ha d'estar feta abans d'instanciar
|
||||||
// Player perquè el spawn consulta `collision_*`.
|
// Player perquè el spawn consulta `collision_*`. Els membres de Player
|
||||||
Player::loadFrom(config_.player_yaml);
|
// 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,
|
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
||||||
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
// 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
|
// 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
|
// 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.
|
// sobre la qual cablejar HUD i fi de nivell més endavant.
|
||||||
entity_manager_.getBus().subscribe("level_complete", [](const Event& /*ev*/) {
|
entity_manager_.getBus().subscribe("level_complete", [](const Event& ev) {
|
||||||
std::cout << "[LEVEL COMPLETE]" << '\n';
|
std::cout << "[LEVEL COMPLETE] player_id=" << ev.player_id << '\n';
|
||||||
});
|
});
|
||||||
entity_manager_.getBus().subscribe("player_hit", [](const Event& ev) {
|
entity_manager_.getBus().subscribe("player_hit", [](const Event& ev) {
|
||||||
const auto* dmg = std::get_if<int>(&ev.payload);
|
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
|
// Controls per jugador: P1 amb fletxes, P2 amb WASD. Cada Player
|
||||||
// segon només requereix afegir una altra entrada amb uns Controls
|
// escolta les seues pròpies tecles dins del seu `update`, no hi ha
|
||||||
// distints (p.ex. WASD).
|
// estat global d'input.
|
||||||
const Player::Controls CONTROLS_P1{
|
const Player::Controls CONTROLS_P1{
|
||||||
.up = Defaults::KeysGame::UP,
|
.up = Defaults::KeysGame::UP,
|
||||||
.down = Defaults::KeysGame::DOWN,
|
.down = Defaults::KeysGame::DOWN,
|
||||||
.left = Defaults::KeysGame::LEFT,
|
.left = Defaults::KeysGame::LEFT,
|
||||||
.right = Defaults::KeysGame::RIGHT,
|
.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ó
|
// 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
|
// 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
|
// 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);
|
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_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);
|
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
|
// Índex de tile amb què pinta cada jugador — llegit del YAML del mapa
|
||||||
// (camps opcionals `player1_paint_tile` / `player2_paint_tile`). Si no
|
// (camps opcionals `player1_paint_tile` / `player2_paint_tile`). Si no
|
||||||
// hi són, `paint_tile_index_ = -1` i el Player no deixa rastre.
|
// 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);
|
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
|
// PaintLayer: font única del pintat del nivell. Es crea després de
|
||||||
// `loadFromLevel` perquè l'EntityManager ja ha de tenir els sarcòfags
|
// `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`.
|
// manager per subscriure's a `on_tile_entered` i publicar `tile_painted`.
|
||||||
paint_layer_ = std::make_unique<PaintLayer>(tilemap_, &entity_manager_.getBus());
|
paint_layer_ = std::make_unique<PaintLayer>(tilemap_, &entity_manager_.getBus());
|
||||||
|
|
||||||
// Connectem el Player al bus perquè emeti `on_tile_entered` i el
|
// Connectem tots els jugadors al bus perquè emeten `on_tile_entered`;
|
||||||
// PaintLayer (subscrit ja) processe el pintat; el fan-out cap als
|
// el PaintLayer (subscrit ja) processa el pintat i el fan-out cap als
|
||||||
// sarcòfags passa per `tile_painted`.
|
// 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
|
// Marcador (HUD) de la franja superior. Reutilitza la font 8bithud i
|
||||||
// la paleta "ui" ja precarregades pel Director — no toca disc des
|
// 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);
|
auto hud_font = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
|
||||||
hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
|
hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
|
||||||
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font);
|
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
|
// 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
|
// (preload al Director), així que playMusic només mou el punter cap a
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "core/scene/scene.hpp"
|
#include "core/scene/scene.hpp"
|
||||||
#include "core/ui/ui_config.hpp"
|
#include "core/ui/ui_config.hpp"
|
||||||
|
#include "game/debug_config.hpp"
|
||||||
#include "game/entities/entity_manager.hpp"
|
#include "game/entities/entity_manager.hpp"
|
||||||
#include "game/map/paint_layer.hpp"
|
#include "game/map/paint_layer.hpp"
|
||||||
#include "game/map/tilemap.hpp"
|
#include "game/map/tilemap.hpp"
|
||||||
@@ -22,13 +23,17 @@ class Text;
|
|||||||
class GameScene : public Scene {
|
class GameScene : public Scene {
|
||||||
public:
|
public:
|
||||||
// Conjunt de rutes que defineixen una piràmide concreta (mapa + tilemap
|
// Conjunt de rutes que defineixen una piràmide concreta (mapa + tilemap
|
||||||
// + sprites del jugador + música). Cada piràmide serà un Config distint;
|
// + sprites dels jugadors + música). Cada piràmide serà un Config distint;
|
||||||
// `defaultConfig()` retorna la primera (Defaults::Pyramid).
|
// `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 {
|
struct Config {
|
||||||
std::string map_yaml;
|
std::string map_yaml;
|
||||||
std::string player_sprite;
|
std::string player_sprite;
|
||||||
std::string player_yaml;
|
std::string player_yaml;
|
||||||
|
std::string player2_sprite;
|
||||||
|
std::string player2_yaml;
|
||||||
std::string music_track;
|
std::string music_track;
|
||||||
|
DebugConfig::StartingPlayers starting_players{DebugConfig::StartingPlayers::BOTH};
|
||||||
};
|
};
|
||||||
|
|
||||||
[[nodiscard]] static auto defaultConfig() -> Config;
|
[[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.
|
// i puntuacions, ampliarem aquí sense tocar callers.
|
||||||
subs_.push_back(bus_.subscribe("player_hit", [this](const Event& ev) {
|
subs_.push_back(bus_.subscribe("player_hit", [this](const Event& ev) {
|
||||||
const auto* dmg = std::get_if<int>(&ev.payload);
|
const auto* dmg = std::get_if<int>(&ev.payload);
|
||||||
// TODO: quan l'event porte player_id, fer dispatch al bloc real.
|
// Dispatch pel `player_id` de l'event. 0 = P1 (bloc LEFT), 1 = P2
|
||||||
// Mentrestant, apliquem el hit a P1 (consistent amb el log del
|
// (bloc RIGHT). Si l'event arriba sense id (-1, event world-wide)
|
||||||
// GameScene actual, que tampoc desambigua el jugador).
|
// fem fallback a P1 per no perdre'l — a la pràctica tots els
|
||||||
constexpr std::size_t IDX = 0;
|
// 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; }
|
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.flash_ms_left = UiConfig::get().hudFlashMs();
|
||||||
blk.shake_ms_left = UiConfig::get().hudShakeMs();
|
blk.shake_ms_left = UiConfig::get().hudShakeMs();
|
||||||
blk.shake_intensity = 1.5F;
|
blk.shake_intensity = 1.5F;
|
||||||
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';
|
<< " lives=" << players_[IDX].lives << '\n';
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,17 @@ namespace Defaults {
|
|||||||
constexpr SDL_Scancode EXIT = SDL_SCANCODE_ESCAPE;
|
constexpr SDL_Scancode EXIT = SDL_SCANCODE_ESCAPE;
|
||||||
} // namespace KeysGame
|
} // 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 {
|
namespace Player {
|
||||||
constexpr float SPEED_PX_PER_S = 60.0F; // Velocitat base del jugador (sense sprint / slow-zones).
|
constexpr float SPEED_PX_PER_S = 60.0F; // Velocitat base del jugador (sense sprint / slow-zones).
|
||||||
} // namespace Player
|
} // namespace Player
|
||||||
@@ -209,6 +220,11 @@ namespace Defaults {
|
|||||||
constexpr const char* MAP_YAML = "data/maps/pyramid.yaml";
|
constexpr const char* MAP_YAML = "data/maps/pyramid.yaml";
|
||||||
constexpr const char* PLAYER_SPRITE = "data/player/sam.gif";
|
constexpr const char* PLAYER_SPRITE = "data/player/sam.gif";
|
||||||
constexpr const char* PLAYER_YAML = "data/player/sam.yaml";
|
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";
|
constexpr const char* MUSIC_TRACK = "pyramid_1.ogg";
|
||||||
} // namespace Pyramid
|
} // namespace Pyramid
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user