# Sistema d'entitats Registre del disseny, els treballs fets i els pendents per al sistema d'entitats (sarcòfags, porta, momia, antorxa, trampes, roca) d'aee_2026. Material de referència per a decisions futures — no és documentació d'API. Estats possibles: `DONE`, `WIP`, `DEFERRED`, `WONTFIX`. ## Convenció de col·lisió i anchors (fase 3) - **`sprite.size`** (YAML entitat): mida en píxels del frame de render. - **`tile_size`** (YAML entitat): footprint lògic en tiles. Només el consumeix Sarcophagus per al perímetre; a la resta és declaratiu. - **`collision: { size: [w, h], offset: [x, y] }`** (opcional al YAML entitat): sub-rect de col·lisió dins del sprite. Si `offset` falta, es calcula automàticament bottom-center (falsa perspectiva típica). Sense bloc, `Entity::getCollisionRect()` = `pixel_rect_` sencer. - **`anchor`** (opcional al YAML entitat): `top_left` (per defecte) o `feet`. `feet` vol dir que el `position: [col, row]` del map és el tile dels peus; el sprite es centra horitzontalment sobre el tile i s'alinea la vora inferior del sprite amb la vora inferior del tile. Els sarcòfags tenen bottom-align hardcoded i no usen `anchor`. --- ## 1. Context i objectiu inicial aee_2026 és un clon tipus Oh Mummy! amb estètica Amidar. Els sarcòfags eren fins ara rectangles purament geomètrics calculats a [Map::getSarcRect](../source/game/map/map.cpp#L67); no hi havia entitats de joc (momies, antorxes, trampes, portes). **Objectiu inicial** (prompt de partida): - Classe base `Entity` amb posició tile+pixel, sprite/animació opcional, `update(dt)`, `render()`, flags, `onPlayerContact`. - Jerarquia: `Decoration` · `Interactable` · `Hazard` (→ `StaticHazard`, `DynamicHazard`). - 7 classes concretes: `Torch`, `Sarcophagus`, `Door`, `Mummy`, `RollingRock`, `WallTrap`, `FloorTrap`. - `Animation` com a **component** reutilitzable (composició, no herència). - `EntityFactory`/`EntityLoader` amb registre per nom — afegir una entitat no ha de tocar el factory. - Sarcòfag amb perímetre en tiles i estat CLOSED/OPENING/OPEN; emet `sarcophagus_opened`. - Porta amb estat CLOSED/OPENING/OPEN; s'obri quan tots els sarcs oberts; contacte → `level_complete`. - Momia amb IA `corridor_wanderer` (hook virtual). - `EntityManager` + event bus minimalista (pub/sub). - Placeholders gràfics (GIF paletitzats) + YAMLs de plantilla per cada entitat. - Smoke tests stand-alone (sense frameworks externs). --- ## 2. Fase 1 — Andamiatge pur (DONE) Codi, dades i tests aïllats del flux viu del joc: el sistema existeix i funciona, però `GameScene` encara no l'instancia. ### Fitxers creats | Path | Paper | |------|-------| | [source/game/entities/entity.hpp](../source/game/entities/entity.hpp) / [.cpp](../source/game/entities/entity.cpp) | Classe base; `update`, `render`, `onPlayerContact(Player*)`, `notifyTileEntered` | | [source/game/entities/decoration.hpp](../source/game/entities/decoration.hpp) | Header-only; `collides=false`, `interactable=false` | | [source/game/entities/interactable.hpp](../source/game/entities/interactable.hpp) | Header-only; afegeix `interact(Player*)` | | [source/game/entities/hazard.hpp](../source/game/entities/hazard.hpp) | `Hazard` + `StaticHazard` (cicle actiu/inactiu) + `DynamicHazard` (velocitat + `step`) | | [source/game/entities/animation_component.hpp](../source/game/entities/animation_component.hpp) / [.cpp](../source/game/entities/animation_component.cpp) | Frames/fps/loop; `tick`, `currentFrame`. Completada d'inici si 1-frame no-loop | | [source/game/entities/event_bus.hpp](../source/game/entities/event_bus.hpp) / [.cpp](../source/game/entities/event_bus.cpp) | Pub/sub amb `std::variant` payload | | [source/game/entities/entity_factory.hpp](../source/game/entities/entity_factory.hpp) / [.cpp](../source/game/entities/entity_factory.cpp) | Macro `REGISTER_ENTITY` + registre estàtic per nom | | [source/game/entities/entity_loader.hpp](../source/game/entities/entity_loader.hpp) / [.cpp](../source/game/entities/entity_loader.cpp) | Cache de plantilles YAML + `readSpawns` | | [source/game/entities/entity_manager.hpp](../source/game/entities/entity_manager.hpp) / [.cpp](../source/game/entities/entity_manager.cpp) | Contenidor + comptador de sarcs + fan-out de tile | | [source/game/entities/concrete/torch.*](../source/game/entities/concrete/torch.hpp) | Decoració animada | | [source/game/entities/concrete/sarcophagus.*](../source/game/entities/concrete/sarcophagus.hpp) | Perímetre tiles ortogonal + estats | | [source/game/entities/concrete/door.*](../source/game/entities/concrete/door.hpp) | Estats + `open()` + emissió level_complete | | [source/game/entities/concrete/mummy.*](../source/game/entities/concrete/mummy.hpp) | `chooseDirection` virtual (aleatori per defecte) | | [source/game/entities/concrete/rolling_rock.*](../source/game/entities/concrete/rolling_rock.hpp) | Línia recta, direcció inicial del YAML | | [source/game/entities/concrete/wall_trap.*](../source/game/entities/concrete/wall_trap.hpp) | `StaticHazard` amb cicle | | [source/game/entities/concrete/floor_trap.*](../source/game/entities/concrete/floor_trap.hpp) | Igual que wall_trap, semàntica distinta | | [tools/entities/generate_placeholders.py](../tools/entities/generate_placeholders.py) | Genera 7 GIFs paletitzats amb Pillow | | [data/entities/\/\.yaml](../data/entities/) + `.gif` | 7 plantilles d'entitat + 7 placeholders | | [data/maps/test_entities.yaml](../data/maps/test_entities.yaml) | Nivell mínim per al smoke | | [tests/test_entities_smoke.cpp](../tests/test_entities_smoke.cpp) | Executable stand-alone (T1–T8, ampliat a T9 a fase 2) | ### Mides i colors dels placeholders | Entitat | Mida px | Color | |---|---|---| | torch | 16×16 | taronja | | door | 16×16 | marró fosc | | mummy | 12×24 | beige | | sarcophagus | **48×16** (3×1 tiles) | daurat | | rolling_rock | 16×16 | gris | | wall_trap | 16×16 | vermell fosc | | floor_trap | 16×16 | vermell clar | ### Decisions clau - **`onPlayerContact(Player*)` amb punter nullable** en comptes de referència: permet smoke tests sense un Player real, i obre la porta a triggers d'àrea sense jugador específic. - **`Sarcophagus` ha rebut dues variants de ctor**: amb i sense ancoratge. La sense ancoratge la crea el factory des d'una plantilla YAML; `place(col, row, predicate)` s'invoca després quan `EntityManager` coneix la posició. - **Registre estàtic** via macro `REGISTER_ENTITY` — perquè funcioni, tots els `concrete/*.cpp` entren directament a `APP_SOURCES` (no a una STATIC lib sense `--whole-archive`). - **Animació d'1 frame no-loop** marcada com a `completed()` des de la construcció: permet que sarcs i portes amb placeholder transitin OPENING → OPEN al següent `update`. ### Smoke tests fase 1 Target CMake `test_entities_smoke` al final de [CMakeLists.txt](../CMakeLists.txt). Cobria T1–T8: - T1 factory: crea tots els 7 tipus per nom. - T2 loader: llegeix spawns de `data/maps/test_entities.yaml`. - T3 event bus: subscribe/publish/unsubscribe amb payloads variats. - T4 perímetre de sarc: 3×1 tiles, 8 perimetrals, notificació → OPENING. - T5 end-to-end: 2 sarcs + 1 door, obrir sarcs → door OPEN → contacte → level_complete. - T6 animation: frames multi + loop + completed. - T7 plantilles: totes les 7 es carreguen sense excepció. - T8 decoration flags. Tots passats al tancament de la fase 1. El binari del joc, sense canvis, continuava arrencant net sota `xvfb-run`. --- ## 3. Fase 2 — Integració al GameScene (DONE) Cableja l'andamiatge al flux normal del joc. En arrencar amb `pyramid.yaml`, les entitats es carreguen, tenen sprite visible i poden reaccionar als moviments del Player. ### Diagnòstic clau La primera proposta va suggerir refactor del sarcòfag a "4 costats en píxels" perquè l'origen del canvas (40, 64) no era múltiple de 16. **Diagnòstic erroni**: el tilemap està en grid 16×16 **relatiu al seu propi origen** (`origin: [-8, 0]` a `pyramid.yaml`), no al canvas. La conversió correcta és: ``` col = floor((player_center_x - tilemap.origin_x) / tile_size) row = floor((player_center_y - tilemap.origin_y) / tile_size) ``` Amb això, el sarc (0,0) amb canvas rect (40, 64, 48, 16) → tile anchor (3, 4), span (3, 1). El model de tiles es manté intacte. ### Canvis aplicats - **`Map` globals noves** ([map.hpp:36-40](../source/game/map/map.hpp#L36-L40)): `tile_size`, `tilemap_origin_x`, `tilemap_origin_y`. Poblats a `loadFrom` des de `yaml["tile_size"]` i `yaml["origin"]`. - **`EventBus` payload** ampliat amb `struct TilePos { int col; int row; }` + overload `publish(name, TilePos)`. - **`Entity`** amb `std::shared_ptr`, `setSurface`/`getSurface`/`setPixelRect`. Per evitar arrossegar `Surface` al smoke, `Entity::render()` és no-op i el blit real el fa `EntityManager::renderAll`, centralitzat. - **`Player`** amb `setEventBus(EventBus*)` + `getCurrentTile()` (fórmula correcta amb `tilemap_origin_*` + `tile_size`). Al final d'`update`, si el tile canvia, publica `on_tile_entered` amb payload `TilePos`. - **`EntityManager::loadFromLevel`** (viu a [entity_manager_loadlevel.cpp](../source/game/entities/entity_manager_loadlevel.cpp), TU separat per aïllar la dependència a `Resource::Cache`): - `sarcophagus` → `sarc_position` → `Map::getSarcRect` → tile anchor per al perímetre. - resta → `position` (tile coords del tilemap) → píxels via `col*tile_size + origin`. - **`EntityManager::renderAll`** (al mateix TU). Blit directe de `Surface::render` a `pixel_rect_`. - **`EntityManager` subscriu `on_tile_entered`** al ctor: quan rep `TilePos`, fa fan-out via `notifyPlayerEnteredTile(col, row)` a totes les entitats. - **`GameScene`** ([game_scene.cpp](../source/game/scenes/game_scene.cpp)): - Membre `EntityManager entity_manager_`. - Ctor: `loadFromLevel(config_.map_yaml)` després de carregar mapa; subscripcions `level_complete` i `player_hit` (log a `std::cout`); `players_.back()->setEventBus(&entity_manager_.getBus())`. - `update`: `entity_manager_.update(DT)` després dels Players (així els sarcs reben els tiles del mateix frame). - `render`: `entity_manager_.renderAll()` entre `tilemap_.render()` i els Players. - **`data/maps/pyramid.yaml`** amb 5 spawns reals: torch (2,4), sarcs (0,0) i (3,4), door (10,14), mummy (10,7). ### Partició per aïllar dependències El smoke test és stand-alone (no `Resource::Cache`, no `Surface`, no audio). Per no arrossegar tot el pipeline al smoke, `EntityManager` es divideix: - [entity_manager.cpp](../source/game/entities/entity_manager.cpp): `ctor`/`dtor`/`add`/`setDoor`/`addSarcophagus`/`update`/`notifyPlayerEnteredTile` — **linkat al smoke**. - [entity_manager_loadlevel.cpp](../source/game/entities/entity_manager_loadlevel.cpp): `loadFromLevel` + `renderAll` — **NOMÉS al binari principal**. Al CMakeLists: `ENTITY_SOURCES` conté els fitxers core; `APP_SOURCES` afegeix `entity_manager_loadlevel.cpp` explícitament fora de `ENTITY_SOURCES`. ### T9 afegit al smoke `testLoadFromLevel` — Map::loadFrom(pyramid), verifica `tile_size=16`, `origin=(-8, 0)`; EntityLoader::readSpawns retorna 5 spawns amb els tipus esperats; la conversió sarc-grid (0,0) → tile anchor (3,4) és correcta; fan-out via bus: visitar els 8 tiles perimetrals d'un sarc a (3,4) mida 3×1 fa passar l'estat a OPENING. ### Verificació de tancament - `make build`: OK, 0 warnings nous. - `./build/test_entities_smoke`: **9/9 OK**. - `ctest --output-on-failure`: 1/1 suite passed. - `timeout 5 xvfb-run ./build/aee_2026`: 5s sense crash, stdout inclou: ``` Surface : torch.gif ... LOADED Surface : sarcophagus.gif ... LOADED (×2) Surface : door.gif ... LOADED Surface : mummy.gif ... LOADED EntityManager: 5 entitats carregades des de data/maps/pyramid.yaml ``` --- ## 4. Memòria durable registrada Durant la fase 1 l'usuari va establir una regla permanent desada a [memory/feedback_smoke_test_mandatory.md](../../../.claude/projects/-home-sergio-gitea-aee-2026/memory/feedback_smoke_test_mandatory.md) (fora del repo): > Sempre que s'afegeixi o modifiqui lògica no trivial, s'ha d'estendre un smoke existent o crear-ne un de nou que exerciti aquella lògica, i executar-lo abans de reportar la tasca com a acabada. Al missatge final cal indicar explícitament si els smoke tests passen. Es respecta tant a la fase 1 com a la 2. --- ## 5. Pendent / DEFERRED ### Gameplay | ID | Descripció | Estat | |---|---|---| | ENT-IA-01 | **Moviment viu de la momia**: el `chooseDirection` està al seu lloc però el `step` no itera interseccions ni valida direcció contra el mapa. La momia es dibuixa estàtica. | DEFERRED | | ENT-IA-02 | **IA de la momia amb bias cap al jugador** (weighted_towards_player). Hook `chooseDirection` deixat preparat; només cal una subclasse/override. | DEFERRED | | ENT-HAZ-01 | **Trampes actives amb dany real al jugador**. `onPlayerContact` publica `player_hit` però encara no hi ha sistema de vides. Caldrà cablejar vides al Player. | DEFERRED | | ENT-ROCK-01 | **Col·lisió de la roca amb sarcs/murs**. `DynamicHazard::step` desplaça sense validar `Map::collides`. Una roca rodant a tota velocitat travessaria sarcs. | DEFERRED | | ENT-DOOR-01 | **Retorn a la pantalla de títol al `level_complete`**. Ara només log a stdout. Pendent cablejar la transició de Scene. | DEFERRED | ### Render i geometria | ID | Descripció | Estat | |---|---|---| | REND-01 | **Z-order fi** entre entitats i Player (interleaving per `feetY`). Ara les entitats sempre queden sota el Player. Veure comentari a [game_scene.cpp:109-110](../source/game/scenes/game_scene.cpp#L109-L110). | DEFERRED | | REND-02 | **Animacions multi-frame**. `renderAll` clipa el frame actual via `src_rect` calculat amb `Entity::getAnimation()->currentFrame()` sobre una graella left-to-right / top-to-bottom; columnes deduïdes de `surface->getWidth() / pixel_rect.w`. Así un strip 1×N, N×1 o N×M funciona igual. Torch primer consumidor real (4 frames @ 8 fps). | DONE | | REND-03 | **Paleta compartida** amb el tilemap. Cada GIF placeholder té la seua paleta pròpia (Pillow); funciona perquè `Surface` desa paleta per surface. Amb l'art real pot caldre unificar per evitar mismatch cromàtic. | DEFERRED | ### Perímetre i col·lisió | ID | Descripció | Estat | |---|---|---| | PER-01 | **`blocked_predicate` al `Sarcophagus`** per descartar tiles ocupats per altres sarcs. Actualment `loadFromLevel` passa `{}`. Els spawns de demo eviten sarcs veïns; al primer disseny amb 4×5 sarcs adjacents caldrà connectar-ho (amb `Map::collides` traduït a tiles). | DEFERRED | | PER-02 | **Perímetre diagonal opcional**. Ara el perímetre és ortogonal (N/S/E/O). Si volem variants amb cantonades exigides, es pot afegir una flag al YAML de l'entitat. | WONTFIX (per ara — el comportament Oh Mummy! clàssic és ortogonal) | ### Infraestructura | ID | Descripció | Estat | |---|---|---| | INF-01 | **Dead-code elimination del registre estàtic**. Funciona mentre `concrete/*.cpp` viuen a `APP_SOURCES`. Si algun dia es modularitza a STATIC libs caldrà `--whole-archive` o un `entity_registry_bootstrap.cpp` amb cridades explícites. | WONTFIX (vigilar) | | INF-02 | **Multiplayer**: el bus del nivell és únic. Quan hi hagi múltiples Players, cal decidir si el `on_tile_entered` porta també el player_id. Ampliació trivial de `TilePos` o struct nou. | DEFERRED | | INF-03 | **Integrar animacions multi-frame al YAML**: els camps `frames: [0]` actuals són placeholders. Quan arribi l'art, el loader ja és tolerant — només cal omplir els vectors. | WONTFIX (ja previst al codi) | | INF-04 | **Hook `on_tile_entered` emet cada frame que el tile canvia**, no cada frame que el jugador es mou. Si algun dia es vol granularitat sub-tile (p.ex. "passar pel centre exacte"), caldrà un event separat. | WONTFIX | ### Tests | ID | Descripció | Estat | |---|---|---| | TST-01 | **Test d'integració amb `loadFromLevel` real**: el smoke actual evita el Cache per no arrossegar dependències. Un test separat amb el Cache enllaçat podria verificar el blit, però demana instanciar Screen/Canvas. | WONTFIX (l'smoke d'arrencada sota `xvfb-run` ja cobreix el camí viu) | | TST-02 | **Test de moviment de la momia** (quan ENT-IA-01 estigui fet). Pendent juntament amb la IA. | DEFERRED | --- ## 6. Estat global - **Andamiatge i integració bàsica**: DONE. Els 5 actors del `pyramid.yaml` es carreguen, s'assignen surfaces i es renderitzen estàticament sobre el mapa. El bus connecta Player ↔ sarcs ↔ porta. - **Comportament dinàmic (IA, dany, transicions de scene)**: DEFERRED a iteracions posteriors. El hooks existeixen, falta omplir la lògica viva. - **Smoke tests**: 9/9 passant; regla durable de cobrir amb smoke tota lògica no trivial respectada a les dues fases. El document és viu: cada tasca nova hauria d'afegir-s'hi (entrada amb ID, descripció, estat) al fer-la o ajornar-la.