Files
aee-arcade/source/game/entities/entity_manager.hpp
T
JailDesigner a29ea641ab refactor(em): un sol vector entities_ — polimorfisme per la
discriminació

Reverteix la separació en sacs introduïda al commit 62f4b85 (rama
anterior). Amb els virtuals afegits al commit precedent
(`onWhipHit`, `onJustSpawned`, `shouldReceiveContact`, `onRemoved`),
ja no cal separar `map_entities_` i `actors_`: cada entitat decideix
internament què fa, el manager itera un sol vector i confia en el
dispatch virtual.

Canvis:

- `entities_` torna a ser el contenidor únic. Es desfà el dispatch
  `dynamic_cast<DynamicHazard*>` que `add()` feia per enrutar.
- `addSarcophagus`, `removeEntity`, `update`, `notifyPlayerTilePainted`,
  `findFloorTriggerAt`, `renderAll`, `reload` simplificats: un sol
  bucle, sense distingir categories.
- `checkPlayerCollisions` només recorre `entities_` cridant
  `shouldReceiveContact` polimòrfic — la lògica de "saltar SPAWN"
  viu a `DynamicHazard::shouldReceiveContact`.
- `getMapEntities()` i `getActors()` desapareixen. Es manté
  `getEntities()` com a accessor únic.
- `WhipAttack::applyDamage` itera `getEntities()` cridant `onWhipHit`
  polimòrfic. Les entitats no-whippables retornen false (cost: una
  crida virtual buida) i el callsite no discrimina tipus.
- `GameScene::render` mergeja entities + players (dos transforms en
  comptes de tres) per al z-order.

`pending_actors_` es manté: és un concepte ortogonal — entitats que
encara no s'han instanciat al món fins que GameScene les "spawneja"
al text-exit de ROOM_INTRO.

Beneficis del nou disseny:
- Cap dynamic_cast dispers per a la lògica de joc (només queden els
  inevitables: validació de factory i ruteig de la Door).
- Afegir un nou enemic implica `class X : public DynamicHazard` i
  override dels virtuals rellevants — res al manager.
- Si un sarc, antorxa, o decoració futura volgués reaccionar al
  fueteig (trencar-se, encendre's), basta amb un `onWhipHit` override
  i no cal tocar `WhipAttack::applyDamage`.
2026-05-11 13:14:53 +02:00

216 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:
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};
};