From 811c6c482df75b6a7756c087ab285fa40b3fb916 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 26 Apr 2026 10:30:31 +0200 Subject: [PATCH] feat(game-scene): spawn lligat a door_col i respawn a OUTRO->INTRO --- source/game/scenes/game_scene.cpp | 86 ++++++++++++++++++++++++------- source/game/scenes/game_scene.hpp | 19 +++++++ 2 files changed, 87 insertions(+), 18 deletions(-) diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 179dee4..7204e11 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -310,23 +310,12 @@ GameScene::GameScene(Config config) .right = Defaults::KeysGameP2::RIGHT, .jump = Defaults::KeysGameP2::JUMP, }; - // 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 - // cap sarcòfag. - constexpr auto CORRIDOR_CENTER_X = static_cast(GameCanvas::CENTER_X); - const auto CORRIDOR_CENTER_Y = - 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. + // Spawn lligat a la porta: tots els jugadors apareixen davant del + // passadís de sortida. `computeSpawn` deriva les coordenades de + // `Map::door_col` perquè canviar la columna de la porta al YAML + // moga automàticament l'spawn — no hi ha cap valor hardcoded. + const auto SPAWN = computeSpawn(); 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 @@ -338,11 +327,13 @@ GameScene::GameScene(Config config) 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)); + const float X = BOTH ? SPAWN.p1_x : SPAWN.p1_x + static_cast(Map::tile_size); + players_.emplace_back(std::make_unique(anim_p1, ID_P1, 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)); + const float X = BOTH ? SPAWN.p2_x : SPAWN.p2_x - static_cast(Map::tile_size); + players_.emplace_back(std::make_unique(anim_p2, ID_P2, X, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT)); } // PaintLayer: font única del pintat del nivell. Es crea després de @@ -417,11 +408,70 @@ GameScene::GameScene(Config config) GameScene::~GameScene() = default; +auto GameScene::computeSpawn() -> PlayerSpawn { + // Centre de la columna del passadís en píxels canvas. La caixa de + // col·lisió del jugador (12 px) cap dins del passadís (16 px) amb 2 + // px de marge a banda i banda; alinear el sprite al centre de la + // columna garanteix que la base del jugador i la del passadís + // coincideixin verticalment. + const float DOOR_CENTER_X = static_cast((Map::door_col * Map::tile_size) + Map::tilemap_origin_x) + (static_cast(Map::tile_size) / 2.0F); + const float CENTER_TL_X = DOOR_CENTER_X - (static_cast(Player::sprite_w) / 2.0F); + + // Y: centre del corredor exterior superior (mateix valor que abans; + // el corredor és simètric respecte a `playfield_y`). + const float CORRIDOR_CENTER_Y = static_cast(Map::playfield_y) + (static_cast(Map::outer_corridor) / 2.0F); + const float SPAWN_Y = CORRIDOR_CENTER_Y - static_cast(Player::collision_y_offset) - (static_cast(Player::collision_size) / 2.0F); + + // Mode BOTH: P1 a -tile_size, P2 a +tile_size respecte al centre. + // Els callsites individuals decideixen si usar aquests valors o si + // estem en mode single-player (un sol jugador → centrat exactament). + return { + .p1_x = CENTER_TL_X - static_cast(Map::tile_size), + .p2_x = CENTER_TL_X + static_cast(Map::tile_size), + .y = SPAWN_Y, + }; +} + +void GameScene::resetRoom() { + // Recarregar les entitats des del YAML del mapa: la porta torna a + // CLOSED (amb l'open=false a Map al loadFrom), els sarcs tornen a + // CLOSED amb visited buit, momies i altres actors a la posició inicial. + Map::loadFrom(config_.map_yaml); + entity_manager_.reload(config_.map_yaml); + entity_manager_.setExpectedPlayerCount(static_cast(players_.size())); + if (paint_layer_) { paint_layer_->clear(); } + + // Respawn dels jugadors. Recalcular spawn per si door_col canvia + // (avui el mapa no canvia entre cicles, però la lògica ja queda + // preparada per a l'itinerari futur). + const auto SPAWN = computeSpawn(); + const bool BOTH = (players_.size() >= 2); + // Mode single-player: l'únic jugador queda centrat (= SPAWN.p1_x + + // tile_size, perquè SPAWN.p1_x és el centre menys un tile). + const float SINGLE_X = SPAWN.p1_x + static_cast(Map::tile_size); + for (std::size_t i = 0; i < players_.size(); ++i) { + float x = SINGLE_X; + if (BOTH) { x = (i == 0) ? SPAWN.p1_x : SPAWN.p2_x; } + players_[i]->respawn(x, SPAWN.y); + } + + std::cout << "[ROOM] reset: entities reloaded, paint cleared, " + << players_.size() << " player(s) respawned\n"; +} + void GameScene::setState(RoomFsm::State new_state) { if (new_state == state_) { return; } std::cout << "[ROOM] " << RoomFsm::stateName(state_) << " -> " << RoomFsm::stateName(new_state) << '\n'; + const auto PREV = state_; state_ = new_state; transition_ = {}; + // En tornar a ROOM_INTRO des de ROOM_OUTRO (placeholder de + // "torna a la mateixa sala"), reseteja completament l'estat del + // nivell. Si la transició és la inicial (PREV també és ROOM_INTRO, + // imposible amb el guard de dalt) o ve d'un altre estat, no fa res. + if (new_state == RoomFsm::State::ROOM_INTRO && PREV == RoomFsm::State::ROOM_OUTRO) { + resetRoom(); + } } void GameScene::updateRoomTransition(int delta_ms) { diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 1f85996..b8f1149 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -168,6 +168,25 @@ class GameScene : public Scene { void renderRoomFade(); void renderRoomText(); + // Posició inicial dels jugadors derivada de `Map::door_col`. Tots + // els jugadors comparteixen Y (centre del corredor exterior superior); + // X depèn del nombre actiu: amb un jugador queda centrat sota la + // porta; amb dos, separats ±tile_size respecte al centre. Estructura + // exposada perquè els tests puguen verificar-la sense entrar al ctor + // sencer. + struct PlayerSpawn { + float p1_x{0.0F}; + float p2_x{0.0F}; + float y{0.0F}; + }; + [[nodiscard]] static auto computeSpawn() -> PlayerSpawn; + + // Reset complet de la sala: reload de l'EntityManager (sarcs/porta/ + // momies des de zero), neteja del PaintLayer i respawn dels jugadors + // a la posició calculada per `computeSpawn`. Cridat des de + // `setState(ROOM_INTRO)` quan la transició anterior era OUTRO. + void resetRoom(); + #ifdef _DEBUG // Editor modal del tilemap. Mútuament excloent amb la pausa: si la // pausa no està IDLE la tecla de toggle de l'editor s'ignora, i