221 lines
11 KiB
C++
221 lines
11 KiB
C++
#pragma once
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
#include <cstddef>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "external/fkyaml_node.hpp"
|
|
#include "game/entities/event_bus.hpp"
|
|
|
|
class Entity;
|
|
class Sarcophagus;
|
|
class Door;
|
|
class Player;
|
|
class DynamicHazard;
|
|
|
|
// Contenidor i coordinador d'entitats d'un nivell. Responsabilitats:
|
|
// 1. `add` registra una entitat, la connecta al bus i manté propietat.
|
|
// 2. `update(dt)` propaga el tick a totes.
|
|
// 3. Comptador de sarcòfags oberts — quan iguala el total registrat,
|
|
// crida `door_->open()` automàticament.
|
|
// 4. `notifyPlayerTilePainted(col, row)` — fan-out del hook per a tots
|
|
// els sarcòfags (i qualsevol entitat futura que s'hi subscrigui via
|
|
// `notifyTilePainted`).
|
|
//
|
|
// NO és un singleton: es crea una instància per nivell. De moment no
|
|
// s'usa encara al GameScene (andamiatge pur).
|
|
class EntityManager {
|
|
public:
|
|
EntityManager();
|
|
~EntityManager();
|
|
|
|
EntityManager(const EntityManager&) = delete;
|
|
EntityManager(EntityManager&&) = delete;
|
|
auto operator=(const EntityManager&) -> EntityManager& = delete;
|
|
auto operator=(EntityManager&&) -> EntityManager& = delete;
|
|
|
|
// Registre d'una entitat. L'ownership és del manager; retorna
|
|
// l'observador per conveniència (no eximeix que el manager la destrueixi).
|
|
auto add(std::unique_ptr<Entity> entity) -> Entity*;
|
|
|
|
// Marca una entitat com a porta principal del nivell. N'hi ha només
|
|
// una. Si ja n'hi ha una assignada, la substitueix (útil a tests).
|
|
void setDoor(Door* door);
|
|
|
|
// Treu i destrueix l'entitat observada. Si era la porta o un sarc,
|
|
// ajusta `door_` / `total_sarcs_` en conseqüència. Pensat per al
|
|
// editor: no toca `opened_sarcs_` ni publica events — el joc està
|
|
// congelat i, en sortir del editor, GameScene torna a fer reload(),
|
|
// així que l'estat es deriva net.
|
|
auto removeEntity(Entity* observer) -> bool;
|
|
|
|
// Marca una entitat com a sarcòfag — si no es passa per `add` amb
|
|
// type=sarcophagus no es compta al total. Útil en combinació amb
|
|
// `add` quan es fa addSarcophagus(unique_ptr<Sarcophagus>).
|
|
auto addSarcophagus(std::unique_ptr<Sarcophagus> sarc) -> Sarcophagus*;
|
|
|
|
// Tick de totes les entitats.
|
|
void update(float delta_time);
|
|
|
|
// Dibuix de totes les entitats. Sense ordenació per z: la crida dura
|
|
// l'ordre d'inserció. GameScene fa el z-order interleaving amb els
|
|
// players directament via `renderEntity`; `renderAll` queda com a
|
|
// dreçera quan no hi ha players al render (editor, tests).
|
|
void renderAll();
|
|
|
|
// Render d'una única entitat (mateixa lògica de clipping per frame
|
|
// que renderAll). Exposat perquè GameScene pugui interleavar entitats
|
|
// i players per `getFeetY()` (REND-01). Estàtic perquè la lògica de
|
|
// render no depèn de cap estat del manager — ara com ara, l'entitat
|
|
// porta la seua surface, animació i pixel_rect.
|
|
static void renderEntity(const Entity& entity);
|
|
|
|
// Accés als punters observacionals a les entitats per a consumidors
|
|
// externs que necessiten iterar (z-order, aplicar dany, etc.). El
|
|
// polimorfisme dins de cada entitat fa la discriminació: l'iterador
|
|
// recorre tot i les entitats que no hagin de respondre tornen no-op
|
|
// (p.ex. Torch::onWhipHit retorna false sense efecte). Sense sacs
|
|
// separats — el `if (entity_is_X)` viu dins de l'entitat, no del caller.
|
|
[[nodiscard]] auto getEntities() const -> std::vector<Entity*>;
|
|
|
|
// Comprova contacte del(s) jugador(s) amb entitats `collides()=true`.
|
|
// Si la caixa base del jugador solapa amb la caixa de col·lisió de
|
|
// l'entitat, crida `entity->onPlayerContact(player)`. No fa res si
|
|
// la llista està buida. Es crida des de GameScene cada frame, després
|
|
// de `update`, perquè la resposta (p.ex. `player_hit`, `level_complete`)
|
|
// reflecteixi l'estat del frame actual.
|
|
void checkPlayerCollisions(const std::vector<Player*>& players);
|
|
|
|
// Cerca la primera entitat que (a) sigui decal de sòl
|
|
// (`is_floor_decal_=true`), (b) admeti triggers d'àrea
|
|
// (`triggers_on_overlap_=true`), i (c) la seva caixa de col·lisió
|
|
// solape amb el rect donat. Pensat per a `Player::integrateJump`,
|
|
// que la consulta a l'aterratge per detectar caigudes sobre Hole
|
|
// (i en el futur pinchos plans, plaques de pressió, charcos...) i
|
|
// saltar-se el LANDING_STUN normal. Retorna `nullptr` si no hi ha
|
|
// cap match — el caller usa el camí d'aterratge habitual.
|
|
[[nodiscard]] auto findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity*;
|
|
|
|
// Fan-out del hook de pintat: per a cada sarcòfag, cridar
|
|
// notifyTilePainted. Les entitats no-sarcòfag ho ignoren per defecte
|
|
// (veure Entity::notifyTilePainted). Invocat com a reacció a
|
|
// `tile_painted` al bus (re-publicat pel PaintLayer un cop filtrat).
|
|
void notifyPlayerTilePainted(int col, int row);
|
|
|
|
// Carrega els spawns d'un YAML de nivell i els afegeix al manager:
|
|
// 1. Llegeix el bloc `entities:` via EntityLoader.
|
|
// 2. Per cada spawn: crea via factory, carrega la seva surface al
|
|
// Resource::Cache, calcula pixel_rect_ segons el tipus
|
|
// (sarc: Map::getSarcRect; altres: col/row del tilemap) i l'afegeix.
|
|
// 3. Loguea `Entities: N loaded` per facilitar el smoke d'arrencada.
|
|
// Requereix que Map::loadFrom ja s'hagi cridat (necessita
|
|
// `Map::tile_size`, `tilemap_origin_*` i `getSarcRect`).
|
|
void loadFromLevel(const std::string& level_yaml_path);
|
|
|
|
// Crea i afegeix una entitat a partir d'un node spawn (mateix format
|
|
// que els items del bloc `entities:` del YAML del nivell). Retorna el
|
|
// punter observacional a l'entitat creada, o `nullptr` si la plantilla
|
|
// no es pot carregar, el tipus no està registrat o el spawn no té els
|
|
// camps requerits (`position`, `sarc_position`, etc. segons el tipus).
|
|
// Pensat per al editor de Debug: construeix un node sintètic des
|
|
// d'una posició del cursor i el passa.
|
|
auto addSpawn(const fkyaml::node& spawn) -> Entity*;
|
|
|
|
// Instancia tots els actors que estan en `pending_actors_` (cua que
|
|
// `loadFromLevel` omple amb les DynamicHazards declarades al YAML del
|
|
// mapa). Cada actor neix en estat SPAWN (per defecte des de la
|
|
// plantilla; veure `Mummy::Mummy`) i s'arma immediatament el
|
|
// `triggerSpawnExit()` perquè la fase EMERGING corri els seus segons
|
|
// de `spawn.duration_s` i transiti a ACTIVE. Vacia la cua. Cridada
|
|
// des de GameScene en una subfase concreta de ROOM_INTRO i des de
|
|
// `respawnPlayersInstant` (salt debug).
|
|
void spawnPendingActors();
|
|
|
|
// Crea un actor on-demand i el deixa en EMERGING (com a
|
|
// `spawnPendingActors` però per a un sol). Pensat per a futurs
|
|
// generadors (StaticHazards que produeixen DynamicHazards en runtime,
|
|
// p.ex. gotes que cauen, roques que rodolen). Retorna el punter
|
|
// observacional o `nullptr` si la plantilla o el factory fallen.
|
|
auto spawnActor(const std::string& type, int col, int row) -> DynamicHazard*;
|
|
|
|
// Reset complet del nivell: destrueix totes les entitats, reseteja
|
|
// els comptadors interns (sarcòfags oberts, total, exited, flag de
|
|
// level_complete) i torna a cridar `loadFromLevel` per regenerar les
|
|
// entitats des del YAML. El bus i les seues subscripcions
|
|
// sobreviuen — això inclou les subscripcions externes (paint_layer_,
|
|
// hud_, players, GameScene). Cridat des de GameScene a la transició
|
|
// ROOM_OUTRO → ROOM_INTRO. `setExpectedPlayerCount` cal cridar-lo de
|
|
// nou després si vols agregació de level_complete al nou cicle.
|
|
void reload(const std::string& level_yaml_path);
|
|
|
|
// Fixa quants jugadors actius es consideren al recompte d'EXITED.
|
|
// Cridat per GameScene un cop instanciats els jugadors. Si manca,
|
|
// `expected_players_ = 0` i mai s'emet `level_complete` automàticament.
|
|
// Cridar-lo de nou al respawn (futur reload) reseteja el comptador.
|
|
void setExpectedPlayerCount(int n);
|
|
|
|
[[nodiscard]] auto getBus() -> EventBus& { return bus_; }
|
|
[[nodiscard]] auto entityCount() const -> std::size_t { return entities_.size(); }
|
|
[[nodiscard]] auto pendingActorCount() const -> std::size_t { return pending_actors_.size(); }
|
|
[[nodiscard]] auto sarcophagusCount() const -> std::size_t { return total_sarcs_; }
|
|
[[nodiscard]] auto openedSarcophagusCount() const -> std::size_t { return opened_sarcs_; }
|
|
[[nodiscard]] auto exitedPlayerCount() const -> std::size_t { return exited_players_; }
|
|
|
|
// Força l'obertura de la porta sense que els sarcòfags estiguin
|
|
// oberts. Pensat com a hook de debug per a la GameScene: el flux
|
|
// posterior (creuar la porta → `player_exited` → `level_complete`
|
|
// → OUTRO) segueix exactament igual perquè depèn de Door, no de
|
|
// sarcs. No-op si la porta ja està oberta o si el nivell no en té.
|
|
void forceOpenDoor();
|
|
|
|
// Hook que `Sarcophagus::onRemoved` crida per decrementar el
|
|
// comptador `total_sarcs_`. Públic perquè el sarcòfag és la única
|
|
// entitat que necessita aquest manteniment, però l'API es manté
|
|
// genèrica: si en el futur Door volgués notificar quelcom semblant,
|
|
// afegim el seu propi callback.
|
|
void onSarcophagusRemoved() {
|
|
if (total_sarcs_ > 0) { --total_sarcs_; }
|
|
}
|
|
|
|
private:
|
|
// Suport per al callback registrat a `Map::setEntityCollider`. Itera
|
|
// les entitats amb `blocksMovement()` i fa overlap estricte (mateixa
|
|
// convenció que `Map::collides`). El callback el registra el ctor.
|
|
[[nodiscard]] auto entityCollidesAt(const SDL_FRect& box) const -> bool;
|
|
|
|
EventBus bus_;
|
|
// Un sol contenidor lògic per a totes les entitats instanciades. La
|
|
// discriminació de tipus la fan els virtuals (`onPlayerContact`,
|
|
// `onWhipHit`, `shouldReceiveContact`, `notifyTilePainted`...) — el
|
|
// manager itera i cada entitat decideix què fa. Hi conviuen
|
|
// decoracions, static_hazards, interactables i dynamic_hazards
|
|
// (actors).
|
|
std::vector<std::unique_ptr<Entity>> entities_;
|
|
// Cua d'actors declarats al YAML del mapa però encara no instanciats:
|
|
// `loadFromLevel` hi empeny tots els DynamicHazards. `spawnPendingActors`
|
|
// o `spawnActor` consumeixen aquesta cua (o creen un actor on-demand)
|
|
// i ho buiden. El `fkyaml::node` original es guarda perquè conté la
|
|
// posició i qualsevol override del map respecte a la plantilla.
|
|
struct PendingActor {
|
|
std::string type;
|
|
fkyaml::node spawn_node;
|
|
};
|
|
std::vector<PendingActor> pending_actors_;
|
|
Door* door_{nullptr};
|
|
std::size_t total_sarcs_{0};
|
|
std::size_t opened_sarcs_{0};
|
|
// Recompte d'`player_exited`: incrementat a la subscripció al bus,
|
|
// resetejat per `setExpectedPlayerCount`. Quan `exited_players_` arriba
|
|
// a `expected_players_` (i tots dos > 0), publica `level_complete`
|
|
// amb `player_id = -1` (event d'agregat, no atribuible a un jugador).
|
|
int expected_players_{0};
|
|
int exited_players_{0};
|
|
bool level_complete_emitted_{false};
|
|
EventBus::SubId sarc_sub_id_{0};
|
|
EventBus::SubId tile_sub_id_{0};
|
|
EventBus::SubId exit_sub_id_{0};
|
|
};
|