Files
aee-arcade/docs/entities-system.md
T

218 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.
---
## 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/\<name\>/\<name\>.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<Surface>`, `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**. Actualment els placeholders són 1 frame; el blit és directe a `pixel_rect_`. Quan arribi l'art real, `renderAll` haurà de clipar per `anim_.currentFrame()` × `sprite.size`. | DEFERRED |
| 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.