Afegit `Map::Info` (POD trivialment copiable) que empaqueta playfield, pitch, grid, tile_size, tilemap_origin + 2 punters a funció (collides, inside_playfield). `Map::makeInfo()` el construeix a partir dels globals del namespace. `Player` ara rep `const Map::Info&` al ctor i en guarda una còpia plana (`map_`), substituint totes les lectures de `Map::*` per `map_.*`. La inclusió `#include "game/map/map.hpp"` puja al header per la dependència de `Map::Info`; player.cpp deixa d'incloure'l (transitiu). Només el Player es desacobla — momies, roques i `loadFromLevel` continuen llegint `Map::*` directament, com diu la nota original del audit (mentre hi hagi un Map global actiu, l'acoblament és acceptable). Si quan s'acabi PATCH-01 algun consumidor extra chirría, COUP-02b en una rama nova. Smoke tests 13/13 OK; log Game start → Bye! sota xvfb-run idèntic al baseline. clang-tidy i cppcheck nets sobre els fitxers modificats. Refs: docs/audit-game.md (COUP-02). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
185 lines
8.1 KiB
C++
185 lines
8.1 KiB
C++
#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/entity.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, Map::makeInfo()));
|
||
|
||
// 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);
|
||
|
||
// Detecció de contacte player↔entitat després que totes les entitats
|
||
// hagin mogut aquest frame. La porta (si està oberta) emet
|
||
// `level_complete`; la momia/trampa emet `player_hit`. Els sarcs, encara
|
||
// que tinguen collides=true, tenen la caixa lògica fora del camí del
|
||
// jugador (gràcies a Map::collides al Player::update), així que aquí
|
||
// no generen falsos contactes.
|
||
std::vector<Player*> player_ptrs;
|
||
player_ptrs.reserve(players_.size());
|
||
std::ranges::transform(players_, std::back_inserter(player_ptrs), [](const auto& p) { return p.get(); });
|
||
entity_manager_.checkPlayerCollisions(player_ptrs);
|
||
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();
|
||
|
||
// Z-order interleaving entitats ↔ players per `getFeetY()` ascendent:
|
||
// els actors amb els peus més amunt queden dibuixats primer (al fons),
|
||
// els més avall al davant. Així un sarc situat per darrere d'un player
|
||
// queda tapat per ell, i un sarc a davant el tapa a ell.
|
||
struct Renderable {
|
||
float feet_y;
|
||
Entity* entity; // nullable; si és nullptr, s'usa `player`.
|
||
Player* player; // nullable; si és nullptr, s'usa `entity`.
|
||
};
|
||
std::vector<Renderable> order;
|
||
order.reserve(entity_manager_.entityCount() + players_.size());
|
||
const auto ENTITIES = entity_manager_.getEntities();
|
||
std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) {
|
||
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
||
});
|
||
std::ranges::transform(players_, std::back_inserter(order), [](const auto& p) {
|
||
return Renderable{.feet_y = p->getFeetY(), .entity = nullptr, .player = p.get()};
|
||
});
|
||
std::ranges::stable_sort(order, [](const Renderable& a, const Renderable& b) {
|
||
return a.feet_y < b.feet_y;
|
||
});
|
||
for (const auto& r : order) {
|
||
if (r.entity != nullptr) {
|
||
EntityManager::renderEntity(*r.entity);
|
||
} else if (r.player != nullptr) {
|
||
r.player->render();
|
||
}
|
||
}
|
||
|
||
Notifier::get().render();
|
||
RenderInfo::get().render();
|
||
Screen::get().present();
|
||
}
|