#pragma once #include #include #include #include #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. virtual void notifyTilePainted(int col, int row); // 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(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::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_ = 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& { 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& { 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& { 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; } protected: std::string name_; TilePos tile_{}; SDL_FRect pixel_rect_{}; std::optional anim_; std::optional collision_box_; std::shared_ptr 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}; bool dead_{false}; SDL_FlipMode flip_{SDL_FLIP_NONE}; EventBus* bus_{nullptr}; EntityManager* entity_manager_{nullptr}; };