#include "game/scenes/game_scene.hpp" #include #include #include #include #include #include #include #include "core/audio/audio.hpp" #include "core/input/global_inputs.hpp" #include "core/rendering/notifier.hpp" #include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" #include "core/resources/resource_cache.hpp" #include "game/entities/player.hpp" #include "game/map/map.hpp" #include "game/options.hpp" #include "game/ui/ui_palette.hpp" #include "utils/defaults.hpp" #include "utils/defines.hpp" namespace { // La geometria ha de quedar inicialitzada abans que Tilemap parsejar el // mateix YAML i abans que el ctor de Player consulte Map::playfield_*. // Construïm el Tilemap via un helper que primer carrega la geometria // i després retorna el Tilemap per valor (move-construït). auto makeTilemap(const std::string& yaml_path) -> Tilemap { Map::loadFrom(yaml_path); return Tilemap(yaml_path); } } // namespace auto GameScene::defaultConfig() -> Config { return { .map_yaml = Defaults::Pyramid::MAP_YAML, .player_sprite = Defaults::Pyramid::PLAYER_SPRITE, .player_yaml = Defaults::Pyramid::PLAYER_YAML, .music_track = Defaults::Pyramid::MUSIC_TRACK, }; } GameScene::GameScene(Config config) : config_(std::move(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); // 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); // 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). const Player::Controls CONTROLS_P1{ .up = Defaults::KeysGame::UP, .down = Defaults::KeysGame::DOWN, .left = Defaults::KeysGame::LEFT, .right = Defaults::KeysGame::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 // 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); players_.emplace_back(std::make_unique(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1)); // 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 // jail_audio i obri l'stream. Audio::get() està garantit no-null: // Director::init crida Audio::init abans d'instanciar GameScene. Audio::get()->playMusic(config_.music_track, -1); } GameScene::~GameScene() = default; void GameScene::update(int delta_ms) { GlobalInputs::update(delta_ms); if (GlobalInputs::wantsQuit()) { wants_quit_ = true; } const float DT = static_cast(delta_ms) / 1000.0F; for (auto& player : players_) { player->update(DT); } RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW)); Notifier::get().update(delta_ms); } void GameScene::render() { const auto BG_ARGB = Resource::Cache::get().getPalette("ui")[UiColor::CANVAS_BG]; Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BG_ARGB); // Mapa visual: textura prerenderitzada del tilemap. Els sarcòfags // queden "foradats" (cel·les transparents) perquè passaran a ser // actors independents; Map::collides encara els bloqueja per a les // col·lisions del jugador. tilemap_.render(); // Amb un sol jugador, l'ordre és irrellevant i ens estalviem el vector // temporal. Quan n'hi haja més d'un (o quan els sarcs siguen sprites) // caldrà ordenar per `getFeetY()` ascendent i interleaveing amb les // files de sarcs — l'ordre ascendent manté els més al fons darrere. if (players_.size() <= 1) { for (auto& player : players_) { player->render(); } } else { std::vector sorted; sorted.reserve(players_.size()); std::ranges::transform(players_, std::back_inserter(sorted), [](const auto& p) { return p.get(); }); std::ranges::sort(sorted, [](const Player* a, const Player* b) { return a->getFeetY() < b->getFeetY(); }); for (auto* player : sorted) { player->render(); } } Notifier::get().render(); RenderInfo::get().render(); Screen::get().present(); }