350 lines
28 KiB
Markdown
350 lines
28 KiB
Markdown
# 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_arcade. 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_arcade é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_arcade`: 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**: `Mummy::step` camina en línia recta validant `Map::collides` + `Map::insidePlayfield` a cada frame. En topar, recull perpendiculars lliures (evitant U-turn llevat de cul-de-sac) i crida `chooseDirection`. Cobert per T13. | DONE |
|
||
| 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` via `EntityManager::checkPlayerCollisions`, 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**. `RollingRock::step` valida `Map::collides`/`insidePlayfield` i s'atura quan bloqueja (setDirection 0,0). No rebota per disseny. | DONE |
|
||
| 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: `GameScene::render` construeix una llista {Entity*, Player*, feetY}, la ordena amb `stable_sort` ascendent i dibuixa per ordre. Els sprites amb peus més amunt queden darrere dels que tenen peus més avall. | DONE |
|
||
| 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**: T13 exercita step lliure (avança) i step contra paret (gira sense avançar). | DONE |
|
||
|
||
---
|
||
|
||
## 6. Fase 4 — Moviment i interacció (DONE)
|
||
|
||
Tanca els hooks d'IA i col·lisió que quedaven apuntalats a les fases 1–3.
|
||
|
||
- **`Mummy::step`** implementat: desplaçament recte validant playfield i
|
||
sarcs; gir a cul-de-sac o intersecció via `chooseDirection`.
|
||
- **`RollingRock::step`** igual, però s'atura en lloc de girar.
|
||
- **`Entity::getCollisionRect()` + `getFeetY()`**: caixa AABB encastada al
|
||
sprite (falsa perspectiva, opcional via bloc `collision:` del YAML).
|
||
- **Anchor `feet`** al loader: `position: [col, row]` és el tile dels peus;
|
||
el sprite es centra en X i bottom-aligned en Y.
|
||
- **Sarcòfag amb caixa lògica**: `placeSarcophagus` aplica un `CollisionBox`
|
||
que retalla la meitat superior del sprite (alçada) perquè no dispari
|
||
falsos contactes.
|
||
- **`Map::insidePlayfield(box)`**: valida la caixa candidata dels `step`.
|
||
- **`EntityManager::checkPlayerCollisions(players)`**: detecta solapament
|
||
base-box del player amb `getCollisionRect` de cada entitat amb
|
||
`collides()=true`, i crida `onPlayerContact`. Cablejat al `GameScene::update`.
|
||
- **Z-order**: `GameScene::render` ordena {entitats, players} per `getFeetY()`
|
||
ascendent i dibuixa en aquell ordre (interleaved).
|
||
- **`Player::getBaseBox()`** exposat com a API pública.
|
||
- **Smoke tests**: T12 (CollisionBox), T13 (Mummy::step), T11 (sarc YAML),
|
||
T10 (torch anim). Total 14/14.
|
||
|
||
## 7. Fase 5 — Convenció de caixa "peus" com el jugador + animacions direccionals (DONE)
|
||
|
||
Alinea la momia amb la convenció de col·lisió i animació del Player, i soluciona regressions de navegació derivades del canvi de caixa.
|
||
|
||
### Caixa concèntrica amb el rectangle-base
|
||
|
||
- **`collision.base_size: [bw, bh]`** (nou camp YAML, opcional). Si apareix, la caja no s'alinea a la vora inferior del sprite sinó que queda **concèntrica amb el rectangle-base `bw×bh` dels peus** (els `bh` píxels inferiors del sprite). Fórmula: `off_y = sprite_h − (base_h + H) / 2`. Sense `base_size`, comportament antic (alineada a la vora inferior del sprite).
|
||
- **`anchor: feet` ara alinea la caja, no el sprite**. Si l'entitat té `CollisionBox`, el loader col·loca `pixel_rect_` perquè la caja quedi exactament al tile. Sense caja, fallback a la convenció antiga (sprite centrat horitzontalment + vora inferior al tile). Per la momia: sprite 12×24 a (tile_x+2, tile_y−10); caja 16×16 a (tile_x, tile_y) — omple el tile sencer com fa el jugador.
|
||
- **`mummy.yaml`**: `collision: { size: [16, 16], base_size: [12, 12] }`. Abans era `[12, 12]` alineada a baix; visualment quedava "enganxada al mur inferior" dins dels corredors horitzontals.
|
||
|
||
### Snap-to-edge per girar al playfield
|
||
|
||
La caja 16×16 té exactament l'amplada del corredor (16 px). Amb el moviment contínu `speed × delta_time` (~0.66 px/frame), la momia s'aturava a una posició subpíxel dins de l'última cel·la (p. ex. `x=279.87` en lloc de `x=280`) i qualsevol gir perpendicular col·lidia amb el sarc adjacent al limit. [`Mummy::snapToPlayfieldEdge`](../source/game/entities/concrete/mummy.cpp) redueix el `dx/dy` fins a la vora exacta quan el candidat sortiria del playfield; el gir posterior ja parteix d'una posició alineada amb el lane.
|
||
|
||
### Animacions direccionals
|
||
|
||
- **`mummy.gif` a 12 frames** (graella 12×1) amb el mateix esquema que el jugador: 0–3 walk_down, 4–7 walk_up, 8–11 walk lateral. Sense variants `stand_*` (mai no para).
|
||
- **`Mummy` té tres `AnimationComponent`** privats + un helper `changeDirection(dx, dy)` (únic punt d'entrada per canviar direcció) que selecciona l'animació i aplica el flip: `dx ≠ 0` → lateral amb flip horitzontal segons signe; `dy < 0` → up; `dy > 0` → down.
|
||
- **`Entity::flip_` a la base** + `getFlip()`. `renderEntity` el passa a `Surface::render`, que ja suportava `SDL_FlipMode`. Les entitats estàtiques queden a `SDL_FLIP_NONE` automàticament.
|
||
|
||
### Pipeline de build: sincronització de `data/`
|
||
|
||
Canvi a [CMakeLists.txt](../CMakeLists.txt): la còpia de `data/` al build era un `add_custom_command(POST_BUILD)`, que només es dispara quan el binari es relinkeja. Si s'editava només un YAML o un GIF sense tocar codi, `build/data/` quedava obsolet. Ara és un `add_custom_target(sync_data ALL ...)` amb dependència del target principal — els custom targets sense BYPRODUCTS es consideren sempre out-of-date, així que la còpia s'executa a cada `cmake --build`.
|
||
|
||
### Refactors de suport
|
||
|
||
- `Mummy::step` partit: `snapToPlayfieldEdge`, `collectDirectionPools`, `applyChoice` — per mantenir la complexitat cognitiva sota el llindar de clang-tidy (< 25).
|
||
|
||
## 8. Fase 6 — Pintat de tiles (DONE)
|
||
|
||
Introdueix la mecànica central: el jugador deixa rastre al terra en trepitjar-lo, i els sarcòfags compten tiles **pintats** (no tiles *trepitjats*). Font única de veritat del pintat: una nova capa `PaintLayer` escolta el moviment del Player, filtra pintabilitat i re-publica `tile_painted`, que és el que ara observen els sarcs.
|
||
|
||
### Cadena d'events resultant
|
||
|
||
```
|
||
Player canvi de tile
|
||
→ bus: on_tile_entered { col, row, paint_tile_index }
|
||
→ PaintLayer: filtra Map::isPaintable + "primer pinta guanya"
|
||
→ buffer ARGB + bus: tile_painted { col, row }
|
||
→ EntityManager fan-out → Sarcophagus::notifyTilePainted
|
||
→ perímetre complet → OPENING → OPEN → sarcophagus_opened
|
||
→ tots oberts → Door::open
|
||
```
|
||
|
||
Dos events encadenats perquè el *pintat* no és la *presència*: es filtra per pintabilitat i per "ja pintat", i el re-publish garanteix que els sarcs no vegin falsos positius (p. ex. el jugador lliscant contra un mur, o un tile ja marcat per l'altre jugador).
|
||
|
||
### Canvis per fitxer
|
||
|
||
| Path | Canvi |
|
||
|---|---|
|
||
| [event_bus.hpp](../source/game/entities/event_bus.hpp) | Nou `struct PaintedTile { col, row, tile_index }` al `Payload` variant + overload `publish`. |
|
||
| [map.hpp](../source/game/map/map.hpp) / [.cpp](../source/game/map/map.cpp) | `Map::isPaintable(col, row)` → `true` si el tile cau dins del playfield i no s'encavalca cap sarc. |
|
||
| [map/paint_layer.hpp](../source/game/map/paint_layer.hpp) / [.cpp](../source/game/map/paint_layer.cpp) | Classe nova. Subscriu `on_tile_entered`, marca `painted_[row][col]`, blita el tile al buffer ARGB propi, publica `tile_painted`. `render()` anàleg a `Tilemap::render`. |
|
||
| [player.hpp](../source/game/entities/player.hpp) / [.cpp](../source/game/entities/player.cpp) | Camp `paint_tile_index_` al ctor (default -1). El Player publica `PaintedTile` amb el seu índex en lloc de `TilePos`. |
|
||
| [entity.hpp](../source/game/entities/entity.hpp) / [.cpp](../source/game/entities/entity.cpp) | Rename `notifyTileEntered` → `notifyTilePainted`. |
|
||
| [concrete/sarcophagus.hpp](../source/game/entities/concrete/sarcophagus.hpp) / [.cpp](../source/game/entities/concrete/sarcophagus.cpp) | Mateix rename. La lògica del `visited_` és idèntica — només canvia qui li truca el hook. |
|
||
| [entity_manager.hpp](../source/game/entities/entity_manager.hpp) / [.cpp](../source/game/entities/entity_manager.cpp) | La subscripció canvia d'`on_tile_entered` → `tile_painted`. Fan-out renombrat a `notifyPlayerTilePainted`. |
|
||
| [game_scene.hpp](../source/game/scenes/game_scene.hpp) / [.cpp](../source/game/scenes/game_scene.cpp) | Membre `std::unique_ptr<PaintLayer> paint_layer_` (declarat després d'`entity_manager_` per garantir ordre invers de destrucció). Helper local `readOptionalInt` llegeix `player1_paint_tile` del YAML del mapa i el passa al Player. `render()` crida `paint_layer_->render()` entre `tilemap_.render()` i els actors; s'amaga al mode editor. |
|
||
| [data/maps/pyramid.yaml](../data/maps/pyramid.yaml) | Camps nous `player1_paint_tile: 2` i `player2_paint_tile: 2` (índex provisional, canviable sense recompilar). |
|
||
| [tests/test_entities_smoke.cpp](../tests/test_entities_smoke.cpp) | T9 adaptat: publica `tile_painted` directament al bus per aïllar el fan-out d'EntityManager de la resta de la cadena. |
|
||
|
||
### Decisions de disseny
|
||
|
||
- **Centralitzat vs per-sarc**: l'estat "pintat" viu al `PaintLayer`, no dins cada sarc. Els sarcs només apunten els tiles del seu perímetre que han estat notificats pintats. Aquesta separació deixa preparat per a items-en-sarc (obrir quan `N` items trobats, no quan tots oberts) i per a altres productors de pintat futurs (enemics, trampes).
|
||
- **"Primer pinta guanya"**: `PaintLayer::tryPaint` retorna false si el tile ja està pintat. No se sobreescriu amb l'índex d'un altre jugador. Reduïm fonts de decisió inconsistent; el rastre visual és estable.
|
||
- **Paint tile per jugador**: l'índex l'aporta el YAML del mapa (`player1_paint_tile`, `player2_paint_tile`), no el YAML del jugador. Raó: depèn del tileset del mapa — cada nivell pot oferir rastres diferents; canviar de mapa canvia l'índex vàlid.
|
||
- **Render al mode editor**: desactivat. L'editor edita el tilemap base; la capa de pintat és d'estat de joc, no de dades del nivell. Quan `editor_.active()`, `SKIP_ACTORS=true` i el `paint_layer_->render()` se salta amb la resta d'actors.
|
||
- **Resolució**: 16×16 reutilitzant `Map::tile_size` i l'origen del tilemap. El `PaintLayer` emmagatzema `tile_size_` com a camp propi per facilitar una eventual migració a 8×8 (graella 2× més fina) sense tocar la interfície.
|
||
- **Persistència**: runtime only. No hi ha camp `painted:` al YAML. Entrar al `GameScene` reseteja el pintat.
|
||
- **No cal `is_blocked` al `sarc->place(...)`** per a aquest nivell: l'`outer_corridor` del `pyramid.yaml` és exactament 1 tile, cosa que garanteix que el perímetre de qualsevol sarc (fins i tot els de cantonades) cau íntegrament dins del playfield i és pintable. Quedaria rellevant si el disseny futur posa sarcs sense passadís al voltant — cobert per PER-01.
|
||
|
||
### Verificació
|
||
|
||
- `make debug` net, sense warnings ni de clang-tidy ni de cppcheck als fitxers nous.
|
||
- `./build/test_entities_smoke`: **ALL OK** (14/14). T9 adaptat (publica `tile_painted` en lloc d'`on_tile_entered`); la resta de tests del sistema d'entitats passen sense tocar.
|
||
- `timeout 5 xvfb-run ./build/aee_arcade`: arrenca, log `PaintLayer: 21x15 tiles (336x240 px), tileset data/tilesets/tomb.gif` després de `EntityManager: 27 entitats carregades`, surt amb `Bye!` sense crash.
|
||
|
||
### Pendent derivat
|
||
|
||
| ID | Descripció | Estat |
|
||
|---|---|---|
|
||
| PAINT-01 | **Tile de pintat definitiu** quan l'art estigui madur. Ara `2` provisional al `pyramid.yaml`. Canviar l'índex no demana recompilar. | DEFERRED |
|
||
| PAINT-02 | **Resolució 8×8** si es vol més granularitat. El `PaintLayer` porta el seu propi `tile_size_`; cal propagar-lo al ctor per a no lligar-lo a `Tilemap::getTileSize()`. També cal decidir com es mapen els perímetres dels sarcs a sub-caselles. | DEFERRED |
|
||
| PAINT-03 | **Smoke unit del `PaintLayer`**: "primer pinta guanya", filtre d'`isPaintable`, publicació de `tile_painted`. Ara la cadena està coberta end-to-end via T9 (fan-out del bus) i via l'smoke d'arrencada, però el filtre i la deduplicació no tenen test directe. | DEFERRED |
|
||
| PAINT-04 | **Player 2 cablejat al `GameScene`**: la lectura del YAML i el ctor ja suporten un segon jugador amb `paint_tile_index_` propi; només cal instanciar-lo amb Controls distints (p. ex. WASD) i afegir-lo al vector. | DEFERRED |
|
||
|
||
### Memòria durable afectada
|
||
|
||
INF-02 queda parcialment cobert: el `PaintedTile` ja porta `tile_index` (per jugador), que és una forma d'identificar qui pinta sense haver de propagar un `player_id` explícit. Si en el futur cal saber *qui* (p. ex. per puntuació individual), caldrà afegir el camp; el canvi és local al struct.
|
||
|
||
---
|
||
|
||
## 9. Estat global
|
||
|
||
- **Andamiatge, integració, render animat, falsa perspectiva, moviment, animació direccional de momia, pintat de tiles**: DONE. El `pyramid.yaml` actual (4 torches, 20 sarcs, 1 door, 2 mummies) es carrega, anima, col·loca amb l'anchor correcte, col·lisiona segons caixa lògica, el z-order interleava per peus, la momia gira visualment a cada intersecció, i el jugador deixa rastre al terra.
|
||
- **Pendents majors**: ENT-HAZ-01 (sistema de vides), ENT-DOOR-01 (transició d'escena), ENT-IA-02 (bias IA), PER-01 (perímetre amb blocked_predicate quan els sarcs siguin veïns), PAINT-03 (test unit del PaintLayer), PAINT-04 (Player 2 cablejat).
|
||
- **Smoke tests**: 14/14 passant; regla durable de cobrir amb smoke tota lògica no trivial respectada a totes les fases.
|
||
|
||
El document és viu: cada tasca nova hauria d'afegir-s'hi (entrada amb ID, descripció, estat) al fer-la o ajornar-la.
|