Files
aee-arcade/source/game/entities/entity.hpp
T

296 lines
15 KiB
C++

#pragma once
#include <SDL3/SDL.h>
#include <memory>
#include <optional>
#include <string>
#include "game/entities/animation_component.hpp"
// Forward-declaracions: mantenim aquest header lleuger. `Player` viu a
// `source/game/entities/player.hpp` i només es referencia per referència
// dins `onPlayerContact`; `EventBus` és opcional a cada entitat i viu al
// seu header propi. `Surface` és el backend de render paletitzat del motor.
// `EntityManager` només es referencia per punter (injectat opcionalment
// per a entitats que necessiten consultar el món d'entitats — p.ex. la
// mòmia per saber si el pròxim tile té un decal trigger tipus Hole).
class Player;
class EventBus;
class Surface;
class EntityManager;
class WhipAttack;
// Entitat genèrica del món. Actua com a dada en comú per a tots els actors
// del joc (decoratius, interactables, perills mòbils i estàtics): manté
// posició en tiles i píxels, un component d'animació opcional i flags de
// comportament (collides, interactable). Les subclasses intermèdies
// afegeixen semàntica (veure `decoration.hpp`, `interactable.hpp`,
// `hazard.hpp`) i les concretes materialitzen cada tipus (Sarcophagus,
// Torch, Mummy, etc.).
//
// Decisió de disseny: `Entity` NO hereta d'`AnimatedSprite`. L'animació és
// un component (composició), no una herència, per dues raons: (a) no totes
// les entitats en necessiten (trampes invisibles, triggers), i (b) lligar
// el cicle de vida al de `MovingSprite` imposaria semàntica de moviment
// contínu a actors estàtics. Quan s'hi cablege el render real, es podrà
// optar per tenir un `Sprite` intern també com a component opcional.
class Entity {
public:
struct TilePos {
int col{0};
int row{0};
};
// Sub-rectangle de col·lisió dins de `pixel_rect_`. Pensat per a
// personatges amb falsa perspectiva (sprite alt, caixa AABB petita
// encastada a baix) — mateixa convenció que el Player. `offset_x/y`
// són relatius a la cantonada superior-esquerra del sprite. Si l'entitat
// no té `CollisionBox`, `getCollisionRect()` retorna tot el `pixel_rect_`.
struct CollisionBox {
int offset_x{0};
int offset_y{0};
int width{0};
int height{0};
};
Entity() = default;
Entity(std::string name, TilePos tile, SDL_FRect pixel_rect);
virtual ~Entity() = default;
Entity(const Entity&) = delete;
Entity(Entity&&) = delete;
auto operator=(const Entity&) -> Entity& = delete;
auto operator=(Entity&&) -> Entity& = delete;
// Avanç lògic per frame. La base només avança l'animació si existeix;
// les subclasses poden afegir IA, cicles, moviment, etc.
virtual void update(float delta_time);
// Render a la pantalla. La base és no-op: l'andamiatge no cableja encara
// amb el `Screen` real; un cop integrat al `GameScene`, les subclasses
// o un mòdul de render dedicat dibuixaran sprites segons
// `anim_.currentFrame()`.
virtual void render();
// El jugador ha xocat amb aquesta entitat (col·lisió o proximitat).
// Per defecte no fa res; `Hazard` aplica dany, `Door` oberta publica
// `"level_complete"`, etc. Es passa per punter (nullable) perquè els
// smoke tests poden disparar el contacte sense un Player real, i
// perquè en el futur podria disparar-se per triggers d'àrea sense
// jugador específic.
virtual void onPlayerContact(Player* player);
// Hook que el `EntityManager` invoca per a cada entitat cada vegada
// que un tile es pinta (via PaintLayer, com a conseqüència d'un
// jugador trepitjant un sòl pintable). Només `Sarcophagus` ho consumeix
// (per marcar perímetre); per defecte no-op. `player_id` és l'id del
// jugador que ha pintat el tile, o -1 si la pintada ve del broadcast
// inicial de càrrega de nivell (decals + murs, sense atribució). Els
// consumidors han de filtrar per `player_id >= 0` si necessiten
// atribució.
virtual void notifyTilePainted(int col, int row, int player_id);
// L'entitat ha rebut un fueteig amb solapament. Es passa el punter al
// `WhipAttack` perquè la subclasse pugui llegir-ne direcció o atributs
// (mateix patró que `onPlayerContact(Player*)`). Retorna `true` si
// efectivament ha aplicat dany — el caller en marca el set
// `already_hit_` per evitar segons impactes dins del mateix swing.
// Default: no fa res, retorna `false`. Override a `Mummy` (whippable)
// i a futurs enemics whippables. `RollingRock` no override → roca
// immune al látigo per defecte.
virtual auto onWhipHit(WhipAttack* attack) -> bool;
// Hook just després de néixer al món. El manager el crida quan
// `addSpawn` / `spawnActor` / `spawnPendingActors` instancien una
// entitat. La base no fa res; `DynamicHazard` override per disparar
// el seu `triggerSpawnExit` (anima la fase EMERGING). Substitueix el
// `dynamic_cast<DynamicHazard*>(observer)->triggerSpawnExit()` que el
// manager feia abans — ara el dispatch és polimòrfic. Entitats que
// requereixen una fase d'aparició (mòmia amb animació `raise`)
// l'inicien aquí, NO al ctor: així `materializePendingActorsStatic`
// (editor) les salta i queden visibles directament.
virtual void onJustSpawned();
// Hook just abans que el manager esborri l'entitat (via
// `removeEntity`, típicament al editor). Permet a subclasses
// notificar al manager o publicar events per mantenir invariants
// (p.ex. `Sarcophagus` decrementa `total_sarcs_`). Default no-op.
virtual void onRemoved();
// Decisió polimòrfica de si aquesta entitat ha de rebre el callback
// `onPlayerContact` aquest frame. La base permet contacte si `collides_`
// o `triggers_on_overlap_` estan posats. Subclasses poden afegir
// condicions transitòries (p.ex. `DynamicHazard` retorna `false` mentre
// està en SPAWN). Centralitza el filtre que abans vivia al manager
// amb dynamic_casts/isSpawning explícits.
[[nodiscard]] virtual auto shouldReceiveContact() const -> bool;
[[nodiscard]] auto getName() const -> const std::string& { return name_; }
[[nodiscard]] auto getTilePos() const -> TilePos { return tile_; }
[[nodiscard]] auto getPixelRect() const -> SDL_FRect { return pixel_rect_; }
[[nodiscard]] auto collides() const -> bool { return collides_; }
[[nodiscard]] auto interactable() const -> bool { return interactable_; }
// Marca d'entitat que ha de bloquejar el moviment del jugador (com un
// mur). `collides_=true` només garanteix que l'entitat reb el callback
// `onPlayerContact` quan el jugador la toca, no que el seu rect sigui
// sòlid: dynamic hazards (mòmies, roques) tenen `collides_=true` però
// no han de bloquejar el pas — el contacte resol el dany i la roca es
// mou independentment. Per a "murs passius" (sarcs, columnes, debris)
// posem aquest flag a true i `Map::collides` els tracta com a obstacle.
[[nodiscard]] auto blocksMovement() const -> bool { return blocks_movement_; }
// Trigger d'àrea: l'entitat NO bloqueja el moviment (`collides_=false`)
// però sí vol rebre `onPlayerContact` quan el jugador la solapa. Pensat
// per a hazards "no físics" (forats al sòl, picos retràctils, plaques
// de pressió). El bucle de contacte (`checkPlayerCollisions`) admet
// l'entitat si compleix `collides_ || triggers_on_overlap_`. El filtre
// d'altura per salt (`height_`) i la caixa de col·lisió funcionen igual.
[[nodiscard]] auto triggersOnOverlap() const -> bool { return triggers_on_overlap_; }
void setTriggersOnOverlap(bool v) { triggers_on_overlap_ = v; }
// Decal de sòl: hazards estàtics sense presència vertical (forats,
// plaques de pressió, charcos). Es pinten ABANS de qualsevol actor
// dins del Y-sort de `GameScene` perquè conceptualment són "pintura
// sobre el tile". Implementació: `getFeetY()` retorna un sentinel
// mínim (`std::numeric_limits<float>::lowest()`) per quedar al
// principi del `stable_sort` ascendent — així no calen passades de
// render extra. Les subclasses (p.ex. `Hole`) poden sobreescriure
// `getFeetY` per saltar-se el sentinel en moments concrets (p.ex.
// un forat que està tragant un jugador i ha de pintar-se al damunt).
[[nodiscard]] auto isFloorDecal() const -> bool { return is_floor_decal_; }
void setFloorDecal(bool v) { is_floor_decal_ = v; }
// Marca per a entitats transitòries: efectes (polvet, espurnes,
// explosions...) que viuen una sola animació i s'autoeliminen quan
// acaba. L'`EntityManager` purga totes les entitats amb `dead_=true`
// al final de cada `update`. Les entitats persistents (sarcs, momies,
// portes, jugadors) mai no la posen i hi viuen sempre.
[[nodiscard]] auto isDead() const -> bool { return dead_; }
void markDead() { dead_ = true; }
// Associar el bus una sola vegada (propietat externa, p.ex.
// `EntityManager`). Entitats que no publiquen events (Torch, trampes
// estàtiques de moment) el poden ignorar.
void setEventBus(EventBus* bus) { bus_ = bus; }
// Injecció opcional del manager d'entitats (mateix patró que
// `setEventBus`: el manager s'auto-injecta a `add`/`setDoor`/
// `addSarcophagus`). El consumeixen entitats que volen consultar el
// món d'entitats — avui la mòmia, per saber si el tile següent
// conté un decal trigger (Hole) i evitar caure-hi. Entitats que no
// l'usen el poden ignorar.
void setEntityManager(EntityManager* manager) { entity_manager_ = manager; }
// Assigna la caixa de render en píxels. La invoca `EntityManager` durant
// `loadFromLevel` un cop sap la posició definitiva de l'entitat al mapa.
void setPixelRect(SDL_FRect rect) { pixel_rect_ = rect; }
// Assigna la surface per al dibuix. `shared_ptr` perquè les surfaces
// viuen al `Resource::Cache` i es comparteixen. Si no s'assigna,
// `render()` no fa res.
void setSurface(std::shared_ptr<Surface> surface) { surface_ = std::move(surface); }
// Accessor públic (la viewable rep per tipus complet `shared_ptr`, però
// el valor pot ser nul). L'usa `EntityManager::renderAll` per dibuixar
// sense dependre de la dispatch virtual — el dibuix és centralitzat al
// manager per tal de confinar la dependència cap a `Surface`.
[[nodiscard]] auto getSurface() const -> const std::shared_ptr<Surface>& { return surface_; }
// Accessor del component d'animació. `EntityManager::renderAll` el
// consulta per decidir quin frame del strip vertical clipa al blit.
// Entitats sense animació retornen `nullopt` → frame 0 per defecte.
[[nodiscard]] auto getAnimation() const -> const std::optional<AnimationComponent>& { return anim_; }
// Sobreescriu l'`AnimationComponent` actual. L'usa el loader del nivell
// per aplicar el camp opcional `frame: N` del spawn a entitats amb
// variants estàtiques (debris, columnes, etc.): el spawn fixa una
// animació de 1 frame al índex escollit i el render del manager clipa
// el sub-sprite corresponent. També és útil per a tests que volen
// controlar el frame mostrat sense passar pel YAML.
void setAnimation(AnimationComponent anim) { anim_ = std::move(anim); }
// Alçada del cos de l'entitat en píxels. La consulten els filtres
// de col·lisió per altura: si un jugador en l'aire passa per damunt
// d'aquest valor (jump_z_ > height), no col·lisiona amb l'entitat —
// és la mecànica del salt per esquivar momies, roques, etc. Valor
// 0 = sense altura (no es pot esquivar saltant; per a entitats amb
// collides=false el camp és irrellevant). Per a obstacles que
// representen "murs" (sarcs, portes), el YAML els pot donar un
// valor molt alt (p.ex. 999) per fer-los infranquejables.
[[nodiscard]] auto getHeight() const -> int { return height_; }
void setHeight(int h) { height_ = h; }
// Caixa de col·lisió: per defecte és el `pixel_rect_` sencer (entitats
// AABB-aligned). Si s'ha configurat un `CollisionBox`, retorna el
// sub-rect dins del sprite — vida útil: validar col·lisions amb el
// jugador o el mapa sense que la caixa inclogui "l'alçada" del sprite.
[[nodiscard]] auto getCollisionRect() const -> SDL_FRect;
// Y dels peus: la vora inferior de la caixa de col·lisió. La usarà el
// Z-order per ordenar entitats i jugador de menys a més profunditat
// (REND-01, quan s'implemente). Virtual perquè entitats efímeres
// (com `WhipAttack`) poden delegar al seu owner per quedar al
// mateix bucket d'y-sort que el jugador que les ha llançat.
[[nodiscard]] virtual auto getFeetY() const -> float;
// El loader li assigna una caixa explícita llegida del YAML; sense
// aquesta crida, l'entitat queda sense `CollisionBox` i la caixa de
// col·lisió coincideix amb el sprite.
void setCollisionBox(CollisionBox box) { collision_box_ = box; }
// Accés a la caixa crua (offsets + mida) — el loader la consulta per
// a col·locar el sprite quan `anchor: feet` ha d'alinear la caja (no
// el sprite) amb el tile. Nullopt si l'entitat no en té.
[[nodiscard]] auto getCollisionBox() const -> const std::optional<CollisionBox>& { return collision_box_; }
// Flip del sprite, propagat des del motor d'animació
// (`AnimationComponent::flipMode()`): cap entitat el decideix per
// direcció — el YAML declara `flip_h: true` a l'animació simètrica
// (p. ex. `walk_west`) i el setter d'animació l'aplica al `flip_`.
// El manager el passa a `Surface::render`.
[[nodiscard]] auto getFlip() const -> SDL_FlipMode { return flip_; }
void setFlip(SDL_FlipMode flip) { flip_ = flip; }
// Radi de la llum emesa per aquesta entitat (en píxels del canvas).
// 0 = no emet (cas habitual). El bloc opcional `light: { radius: N }`
// del YAML de la plantilla el pobla via `EntityManager::buildEntity`.
[[nodiscard]] auto getLightRadius() const -> int { return light_radius_; }
void setLightRadius(int r) { light_radius_ = r; }
// Activa la modulació "tipus foc" del radi a la pasada d'il·luminació
// (per a antorxes, fogueres, etc.). El bloc opcional
// `light: { flicker: true }` del YAML el pobla. Per defecte false:
// la llum queda perfectament estable (cas player, mòmia).
[[nodiscard]] auto getLightFlicker() const -> bool { return light_flicker_; }
void setLightFlicker(bool f) { light_flicker_ = f; }
// Centre de la llum en píxels del canvas. Per defecte és el centre del
// `pixel_rect_` (que és la caixa de render: sprite top-left + mida).
// Virtual perquè entitats amb desplaçament visual sobre la posició
// lògica (p.ex. el Player en l'aire, que rebaixa visualment `pos_.y`
// amb `jump_z_`) puguin retornar el centre que realment es veu.
[[nodiscard]] virtual auto getLightCenter() const -> SDL_FPoint;
protected:
std::string name_;
TilePos tile_{};
SDL_FRect pixel_rect_{};
std::optional<AnimationComponent> anim_;
std::optional<CollisionBox> collision_box_;
std::shared_ptr<Surface> surface_{nullptr};
bool collides_{false};
bool interactable_{false};
bool triggers_on_overlap_{false};
bool is_floor_decal_{false};
bool blocks_movement_{false};
int height_{0};
int light_radius_{0};
bool light_flicker_{false};
bool dead_{false};
SDL_FlipMode flip_{SDL_FLIP_NONE};
EventBus* bus_{nullptr};
EntityManager* entity_manager_{nullptr};
};