Files
aee-arcade/source/game/scenes/game_scene.cpp
T

165 lines
7.2 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "game/scenes/game_scene.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cstddef>
#include <filesystem>
#include <iostream>
#include <iterator>
#include <utility>
#include <variant>
#include <vector>
#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/event_bus.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);
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
// `makeTilemap`), perquè consulta `Map::tile_size` i `getSarcRect`.
// Les surfaces de les entitats es carreguen al Resource::Cache aquí.
entity_manager_.loadFromLevel(config_.map_yaml);
// 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("player_hit", [](const Event& ev) {
const auto* dmg = std::get_if<int>(&ev.payload);
std::cout << "[PLAYER HIT] 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).
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<float>(GameCanvas::CENTER_X);
const auto CORRIDOR_CENTER_Y =
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);
players_.emplace_back(std::make_unique<Player>(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1));
// Connectem el Player al bus perquè emeti `on_tile_entered` i els
// sarcòfags rebin les notificacions de pas per tile.
players_.back()->setEventBus(&entity_manager_.getBus());
// 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<float>(delta_ms) / 1000.0F;
for (auto& player : players_) { player->update(DT); }
// El tick d'entitats DESPRÉS del tick dels jugadors: si el jugador ha
// emès `on_tile_entered` aquest frame, l'EntityManager ja l'ha encaminat
// als sarcòfags via la subscripció del bus, i aquest `update` pot
// aplicar la transició OPENING → OPEN en el mateix frame.
entity_manager_.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();
// Entitats (sarcòfags, porta, momia, antorxa...) dibuixades per sobre
// dels tiles però sota dels players. Z-order fi (interleaving per feetY
// amb els sarcs) queda per a una tasca posterior; "sin pulido".
entity_manager_.renderAll();
// 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<Player*> 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();
}