feat(entities): integració del sistema d'entitats al GameScene
This commit is contained in:
1 parent
96b71b0f65
commit
7191a08618
16 files changed
+683
-15
No files matched your search
@@ -82,6 +82,10 @@ set(APP_SOURCES
|
|||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
source/game/map/tilemap.cpp
|
source/game/map/tilemap.cpp
|
||||||
${ENTITY_SOURCES}
|
${ENTITY_SOURCES}
|
||||||
|
# Fitxer només del binari principal: `loadFromLevel` depèn de
|
||||||
|
# Resource::Cache (→ Surface → GIF → àudio). Separat per no arrossegar
|
||||||
|
# les seues dependències al smoke test stand-alone.
|
||||||
|
source/game/entities/entity_manager_loadlevel.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
|
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
|
||||||
@@ -251,6 +255,7 @@ option(BUILD_TESTS "Build smoke tests" ON)
|
|||||||
if(BUILD_TESTS)
|
if(BUILD_TESTS)
|
||||||
add_executable(test_entities_smoke
|
add_executable(test_entities_smoke
|
||||||
tests/test_entities_smoke.cpp
|
tests/test_entities_smoke.cpp
|
||||||
|
source/game/map/map.cpp
|
||||||
${ENTITY_SOURCES}
|
${ENTITY_SOURCES}
|
||||||
)
|
)
|
||||||
target_include_directories(test_entities_smoke PRIVATE
|
target_include_directories(test_entities_smoke PRIVATE
|
||||||
|
|||||||
+24
-8
@@ -64,11 +64,27 @@ data: |
|
|||||||
YZDDBBDDADDDBCDDDDDTY
|
YZDDBBDDADDDBCDDDDDTY
|
||||||
YRXXXXXXXXXXXXXXXXXGY
|
YRXXXXXXXXXXXXXXXXXGY
|
||||||
|
|
||||||
# Spawns d'entitats per a aquest nivell. El carregador de Map ignora aquest
|
# Spawns d'entitats per a aquest nivell. Es llegeixen des de
|
||||||
# bloc; només EntityLoader::readSpawns el consulta. Fase actual: andamiatge;
|
# EntityManager::loadFromLevel al GameScene. Convencions:
|
||||||
# encara no cablejat al GameScene, així que aquests spawns es llegiran com
|
# - sarcophagus → `sarc_position: [sarc_col, sarc_row]` en la graella de
|
||||||
# a dades però no instanciaran res al joc real fins que hi arribi la fase
|
# sarcòfags (4×5). EntityManager calcula el rect píxel via
|
||||||
# d'integració. `position` és [col, row] en la graella de sarcòfags
|
# Map::getSarcRect i l'ancoratge de tile per al perímetre.
|
||||||
# (Map::grid_cols × Map::grid_rows) per a sarcòfags, o [col, row] en la
|
# - la resta (torch, door, mummy, rolling_rock, wall_trap, floor_trap)
|
||||||
# graella de tiles de 16 px per a la resta (to be refinat).
|
# → `position: [tile_col, tile_row]` en coordenades del tilemap
|
||||||
entities: []
|
# (origen `[-8, 0]` per defecte). Pixel = tile*16 + tilemap_origin.
|
||||||
|
#
|
||||||
|
# Dos sarcòfags en cantonades oposades → perímetres aïllats (cap tile en
|
||||||
|
# comú). La porta va al centre inferior, on el tilemap té els tiles
|
||||||
|
# d'obertura (R/G). La momia al corredor central. L'antorxa al costat
|
||||||
|
# superior esquerre per verificar render d'una entitat decorativa.
|
||||||
|
entities:
|
||||||
|
- type: torch
|
||||||
|
position: [2, 4]
|
||||||
|
- type: sarcophagus
|
||||||
|
sarc_position: [0, 0]
|
||||||
|
- type: sarcophagus
|
||||||
|
sarc_position: [3, 4]
|
||||||
|
- type: door
|
||||||
|
position: [10, 14]
|
||||||
|
- type: mummy
|
||||||
|
position: [10, 7]
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
# 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.
|
||||||
@@ -12,9 +12,11 @@ void Entity::update(float delta_time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Entity::render() {
|
void Entity::render() {
|
||||||
// Andamiatge: el render real es cablejarà quan s'integri al GameScene.
|
// No-op de base. El dibuix real el fa `EntityManager::renderAll` (viu
|
||||||
// Les subclasses poden sobreescriure si volen lògica especial (p.ex.
|
// a `entity_manager_loadlevel.cpp`, que sí depèn de `Surface`). Així
|
||||||
// no dibuixar-se en estat inactiu); de moment la base no fa res.
|
// `entity.cpp` no arrossega el pipeline de render al smoke test
|
||||||
|
// stand-alone. Les subclasses futures poden sobreescriure si volen
|
||||||
|
// lògica pròpia (p.ex. render condicional segons estat).
|
||||||
}
|
}
|
||||||
|
|
||||||
void Entity::onPlayerContact(Player* /*player*/) {
|
void Entity::onPlayerContact(Player* /*player*/) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -10,9 +11,10 @@
|
|||||||
// Forward-declaracions: mantenim aquest header lleuger. `Player` viu a
|
// Forward-declaracions: mantenim aquest header lleuger. `Player` viu a
|
||||||
// `source/game/entities/player.hpp` i només es referencia per referència
|
// `source/game/entities/player.hpp` i només es referencia per referència
|
||||||
// dins `onPlayerContact`; `EventBus` és opcional a cada entitat i viu al
|
// dins `onPlayerContact`; `EventBus` és opcional a cada entitat i viu al
|
||||||
// seu header propi.
|
// seu header propi. `Surface` és el backend de render paletitzat del motor.
|
||||||
class Player;
|
class Player;
|
||||||
class EventBus;
|
class EventBus;
|
||||||
|
class Surface;
|
||||||
|
|
||||||
// Entitat genèrica del món. Actua com a dada en comú per a tots els actors
|
// 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é
|
// del joc (decoratius, interactables, perills mòbils i estàtics): manté
|
||||||
@@ -78,11 +80,27 @@ class Entity {
|
|||||||
// estàtiques de moment) el poden ignorar.
|
// estàtiques de moment) el poden ignorar.
|
||||||
void setEventBus(EventBus* bus) { bus_ = bus; }
|
void setEventBus(EventBus* bus) { bus_ = bus; }
|
||||||
|
|
||||||
|
// 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_; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::string name_;
|
std::string name_;
|
||||||
TilePos tile_{};
|
TilePos tile_{};
|
||||||
SDL_FRect pixel_rect_{};
|
SDL_FRect pixel_rect_{};
|
||||||
std::optional<AnimationComponent> anim_;
|
std::optional<AnimationComponent> anim_;
|
||||||
|
std::shared_ptr<Surface> surface_{nullptr};
|
||||||
bool collides_{false};
|
bool collides_{false};
|
||||||
bool interactable_{false};
|
bool interactable_{false};
|
||||||
EventBus* bus_{nullptr};
|
EventBus* bus_{nullptr};
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
#include "game/entities/concrete/sarcophagus.hpp"
|
#include "game/entities/concrete/sarcophagus.hpp"
|
||||||
#include "game/entities/entity.hpp"
|
#include "game/entities/entity.hpp"
|
||||||
|
|
||||||
|
// NOTA: `loadFromLevel` viu a `entity_manager_loadlevel.cpp` per evitar
|
||||||
|
// arrossegar `Resource::Cache`, `Surface` i les seues dependències (GIF,
|
||||||
|
// àudio) a consumidors que només volen la base del manager — en
|
||||||
|
// particular, el smoke test stand-alone.
|
||||||
|
|
||||||
EntityManager::EntityManager() {
|
EntityManager::EntityManager() {
|
||||||
// Subscripció al bus: cada sarcòfag obert incrementa el comptador;
|
// Subscripció al bus: cada sarcòfag obert incrementa el comptador;
|
||||||
// quan iguala el total registrat, obrim la porta. La subscripció
|
// quan iguala el total registrat, obrim la porta. La subscripció
|
||||||
@@ -16,10 +21,19 @@ EntityManager::EntityManager() {
|
|||||||
door_->open();
|
door_->open();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Subscripció al moviment del jugador: quan publica `on_tile_entered`
|
||||||
|
// amb payload `TilePos`, fem fan-out a totes les entitats perquè els
|
||||||
|
// sarcòfags puguin actualitzar el seu `visited_`.
|
||||||
|
tile_sub_id_ = bus_.subscribe("on_tile_entered", [this](const Event& ev) {
|
||||||
|
const auto* tile = std::get_if<TilePos>(&ev.payload);
|
||||||
|
if (tile != nullptr) { notifyPlayerEnteredTile(tile->col, tile->row); }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
EntityManager::~EntityManager() {
|
EntityManager::~EntityManager() {
|
||||||
bus_.unsubscribe(sarc_sub_id_);
|
bus_.unsubscribe(sarc_sub_id_);
|
||||||
|
bus_.unsubscribe(tile_sub_id_);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
||||||
@@ -53,3 +67,8 @@ void EntityManager::update(float delta_time) {
|
|||||||
void EntityManager::notifyPlayerEnteredTile(int col, int row) {
|
void EntityManager::notifyPlayerEnteredTile(int col, int row) {
|
||||||
for (auto& e : entities_) { e->notifyTileEntered(col, row); }
|
for (auto& e : entities_) { e->notifyTileEntered(col, row); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTA: `renderAll` es defineix a `entity_manager_loadlevel.cpp`. Conté
|
||||||
|
// blits via `Surface::render`, que dragaria el pipeline complet de render
|
||||||
|
// (GIF, Canvas, Screen, àudio...) al smoke test. Separar-lo manté el smoke
|
||||||
|
// autocontingut.
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
@@ -47,11 +48,26 @@ class EntityManager {
|
|||||||
// Tick de totes les entitats.
|
// Tick de totes les entitats.
|
||||||
void update(float delta_time);
|
void update(float delta_time);
|
||||||
|
|
||||||
|
// Dibuix de totes les entitats. Sense ordenació per z: la crida dura
|
||||||
|
// l'ordre d'inserció. El z-order fi (interleaving amb el Player per
|
||||||
|
// feetY) queda per a una tasca posterior.
|
||||||
|
void renderAll();
|
||||||
|
|
||||||
// Fan-out del hook de moviment de jugador: per a cada sarcòfag,
|
// Fan-out del hook de moviment de jugador: per a cada sarcòfag,
|
||||||
// cridar notifyTileEntered. Les entitats no-sarcòfag ho ignoren per
|
// cridar notifyTileEntered. Les entitats no-sarcòfag ho ignoren per
|
||||||
// defecte (veure Entity::notifyTileEntered).
|
// defecte (veure Entity::notifyTileEntered).
|
||||||
void notifyPlayerEnteredTile(int col, int row);
|
void notifyPlayerEnteredTile(int col, int row);
|
||||||
|
|
||||||
|
// Carrega els spawns d'un YAML de nivell i els afegeix al manager:
|
||||||
|
// 1. Llegeix el bloc `entities:` via EntityLoader.
|
||||||
|
// 2. Per cada spawn: crea via factory, carrega la seva surface al
|
||||||
|
// Resource::Cache, calcula pixel_rect_ segons el tipus
|
||||||
|
// (sarc: Map::getSarcRect; altres: col/row del tilemap) i l'afegeix.
|
||||||
|
// 3. Loguea `Entities: N loaded` per facilitar el smoke d'arrencada.
|
||||||
|
// Requereix que Map::loadFrom ja s'hagi cridat (necessita
|
||||||
|
// `Map::tile_size`, `tilemap_origin_*` i `getSarcRect`).
|
||||||
|
void loadFromLevel(const std::string& level_yaml_path);
|
||||||
|
|
||||||
[[nodiscard]] auto getBus() -> EventBus& { return bus_; }
|
[[nodiscard]] auto getBus() -> EventBus& { return bus_; }
|
||||||
[[nodiscard]] auto entityCount() const -> std::size_t { return entities_.size(); }
|
[[nodiscard]] auto entityCount() const -> std::size_t { return entities_.size(); }
|
||||||
[[nodiscard]] auto sarcophagusCount() const -> std::size_t { return total_sarcs_; }
|
[[nodiscard]] auto sarcophagusCount() const -> std::size_t { return total_sarcs_; }
|
||||||
@@ -64,4 +80,5 @@ class EntityManager {
|
|||||||
std::size_t total_sarcs_{0};
|
std::size_t total_sarcs_{0};
|
||||||
std::size_t opened_sarcs_{0};
|
std::size_t opened_sarcs_{0};
|
||||||
EventBus::SubId sarc_sub_id_{0};
|
EventBus::SubId sarc_sub_id_{0};
|
||||||
|
EventBus::SubId tile_sub_id_{0};
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
// Implementació d'`EntityManager::loadFromLevel`. Viu en un TU separat
|
||||||
|
// perquè aquesta és l'única via per la qual el manager toca
|
||||||
|
// `Resource::Cache`, `Surface` i les seues dependències (GIF, àudio). Els
|
||||||
|
// consumidors que només volen la base del manager (p.ex. el smoke test
|
||||||
|
// stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer.
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/rendering/surface.hpp"
|
||||||
|
#include "core/resources/resource_cache.hpp"
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "game/entities/concrete/door.hpp"
|
||||||
|
#include "game/entities/concrete/sarcophagus.hpp"
|
||||||
|
#include "game/entities/entity.hpp"
|
||||||
|
#include "game/entities/entity_factory.hpp"
|
||||||
|
#include "game/entities/entity_loader.hpp"
|
||||||
|
#include "game/entities/entity_manager.hpp"
|
||||||
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
|
// Render centralitzat al manager per confinar la dependència cap a
|
||||||
|
// `Surface`. Les entitats exposen `getSurface()` + `getPixelRect()` i el
|
||||||
|
// manager dispatcha el blit. Si una entitat no té surface (trigger
|
||||||
|
// invisible, encara sense sprite) simplement es salta.
|
||||||
|
void EntityManager::renderAll() {
|
||||||
|
for (const auto& e : entities_) {
|
||||||
|
const auto& surface = e->getSurface();
|
||||||
|
if (surface == nullptr) { continue; }
|
||||||
|
const auto RECT = e->getPixelRect();
|
||||||
|
surface->render(static_cast<int>(RECT.x), static_cast<int>(RECT.y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Llegeix `sprite.size` o `sprite.path` d'una plantilla i retorna
|
||||||
|
// (surface_key, pixel_size). Si falta algun camp, torna valors
|
||||||
|
// raonables (empty key → no carrega; size [16,16] per defecte).
|
||||||
|
struct SpriteInfo {
|
||||||
|
std::string key; // clau al Resource::Cache (basename del GIF)
|
||||||
|
std::string full_path; // ruta absoluta per a loadSurface
|
||||||
|
float width{16.0F};
|
||||||
|
float height{16.0F};
|
||||||
|
bool valid{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
auto readSpriteInfo(const fkyaml::node& tmpl, const std::string& type) -> SpriteInfo {
|
||||||
|
SpriteInfo info;
|
||||||
|
if (!tmpl.contains("sprite")) { return info; }
|
||||||
|
const auto& s = tmpl["sprite"];
|
||||||
|
if (s.contains("path")) {
|
||||||
|
const auto REL = s["path"].get_value<std::string>();
|
||||||
|
info.key = std::filesystem::path(REL).filename().string();
|
||||||
|
info.full_path = "data/entities/" + type + "/" + REL;
|
||||||
|
info.valid = true;
|
||||||
|
}
|
||||||
|
if (s.contains("size")) {
|
||||||
|
const auto& sz = s["size"];
|
||||||
|
if (sz.is_sequence() && sz.size() == 2) {
|
||||||
|
info.width = static_cast<float>(sz[0].get_value<int>());
|
||||||
|
info.height = static_cast<float>(sz[1].get_value<int>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coordenades píxel (canvas) d'una cel·la (col, row) del tilemap. El
|
||||||
|
// grid del tilemap està 16-alineat respecte a `tilemap_origin_*`, que
|
||||||
|
// pot ser negatiu per overscan CPC.
|
||||||
|
auto tileToPixel(int col, int row) -> SDL_FPoint {
|
||||||
|
return {
|
||||||
|
.x = static_cast<float>((col * Map::tile_size) + Map::tilemap_origin_x),
|
||||||
|
.y = static_cast<float>((row * Map::tile_size) + Map::tilemap_origin_y),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converteix un rect píxel (canvas) a la seua cantonada superior-esquerra
|
||||||
|
// en coordenades de tile del tilemap. Només per a sarcòfags, que naixen
|
||||||
|
// ancorats a una cel·la exacta de la graella.
|
||||||
|
struct TileAnchor {
|
||||||
|
int col;
|
||||||
|
int row;
|
||||||
|
};
|
||||||
|
auto pixelToTileAnchor(const SDL_FRect& rect) -> TileAnchor {
|
||||||
|
return {
|
||||||
|
.col = (static_cast<int>(rect.x) - Map::tilemap_origin_x) / Map::tile_size,
|
||||||
|
.row = (static_cast<int>(rect.y) - Map::tilemap_origin_y) / Map::tile_size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea l'entitat per factory i li adjunta la surface si n'hi ha. Retorna
|
||||||
|
// nullptr si la plantilla no es pot carregar o si el tipus no està
|
||||||
|
// registrat al factory — el loop principal ja imprimeix el missatge.
|
||||||
|
auto buildEntity(const std::string& type, SpriteInfo& sprite_out) -> std::unique_ptr<Entity> {
|
||||||
|
fkyaml::node tmpl;
|
||||||
|
try {
|
||||||
|
tmpl = EntityLoader::loadTemplate(type);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "EntityManager: " << e.what() << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto entity = EntityFactory::instance().create(type, tmpl);
|
||||||
|
if (entity == nullptr) {
|
||||||
|
std::cerr << "EntityManager: tipus desconegut '" << type << "'\n";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
sprite_out = readSpriteInfo(tmpl, type);
|
||||||
|
if (sprite_out.valid) {
|
||||||
|
Resource::Cache::get().loadSurface(sprite_out.key, sprite_out.full_path);
|
||||||
|
entity->setSurface(Resource::Cache::get().getSurface(sprite_out.key));
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Col·loca i afegeix un Sarcophagus a partir del spawn. `entity` es consumeix
|
||||||
|
// en èxit (ownership transferit al manager); en error es destrueix aquí.
|
||||||
|
// Membre privat amagat dins del TU: `EntityManager::` qualifica, però com
|
||||||
|
// que no hi ha declaració al header, només és visible a aquest fitxer.
|
||||||
|
namespace {
|
||||||
|
auto placeSarcophagus(EntityManager& manager, std::unique_ptr<Entity> entity, const fkyaml::node& spawn) -> bool {
|
||||||
|
if (!spawn.contains("sarc_position")) {
|
||||||
|
std::cerr << "EntityManager: sarcophagus sense sarc_position\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& sp = spawn["sarc_position"];
|
||||||
|
const auto RECT = Map::getSarcRect(sp[0].get_value<int>(), sp[1].get_value<int>());
|
||||||
|
const auto ANCHOR = pixelToTileAnchor(RECT);
|
||||||
|
entity->setPixelRect(RECT);
|
||||||
|
|
||||||
|
// Downcast segur: la factory ens garanteix que "sarcophagus" crea
|
||||||
|
// un `Sarcophagus`. Usant el retorn de `release` al ctor del nou
|
||||||
|
// `unique_ptr` evitem el lint `bugprone-unused-return-value`.
|
||||||
|
if (dynamic_cast<Sarcophagus*>(entity.get()) == nullptr) {
|
||||||
|
std::cerr << "EntityManager: la factory no ha creat un Sarcophagus\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::unique_ptr<Sarcophagus> sarc(static_cast<Sarcophagus*>(entity.release()));
|
||||||
|
sarc->place(ANCHOR.col, ANCHOR.row, {});
|
||||||
|
manager.addSarcophagus(std::move(sarc));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto placeGeneric(EntityManager& manager, std::unique_ptr<Entity> entity, const std::string& type, const SpriteInfo& sprite, const fkyaml::node& spawn) -> bool {
|
||||||
|
if (!spawn.contains("position")) {
|
||||||
|
std::cerr << "EntityManager: " << type << " sense position\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& p = spawn["position"];
|
||||||
|
const auto PX = tileToPixel(p[0].get_value<int>(), p[1].get_value<int>());
|
||||||
|
entity->setPixelRect({.x = PX.x, .y = PX.y, .w = sprite.width, .h = sprite.height});
|
||||||
|
|
||||||
|
if (type == "door") {
|
||||||
|
auto* door_ptr = dynamic_cast<Door*>(entity.get());
|
||||||
|
manager.add(std::move(entity));
|
||||||
|
if (door_ptr != nullptr) { manager.setDoor(door_ptr); }
|
||||||
|
} else {
|
||||||
|
manager.add(std::move(entity));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
||||||
|
const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path);
|
||||||
|
int loaded = 0;
|
||||||
|
|
||||||
|
for (const auto& spawn : SPAWNS) {
|
||||||
|
if (!spawn.contains("type")) { continue; }
|
||||||
|
const auto TYPE = spawn["type"].get_value<std::string>();
|
||||||
|
|
||||||
|
SpriteInfo sprite;
|
||||||
|
auto entity = buildEntity(TYPE, sprite);
|
||||||
|
if (entity == nullptr) { continue; }
|
||||||
|
|
||||||
|
const bool OK = (TYPE == "sarcophagus")
|
||||||
|
? placeSarcophagus(*this, std::move(entity), spawn)
|
||||||
|
: placeGeneric(*this, std::move(entity), TYPE, sprite, spawn);
|
||||||
|
if (OK) { ++loaded; }
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "EntityManager: " << loaded << " entitats carregades des de "
|
||||||
|
<< level_yaml_path << '\n';
|
||||||
|
}
|
||||||
@@ -16,15 +16,23 @@ class Entity;
|
|||||||
// Event pub/sub minimalista. Hi ha un únic `EventBus` per mundus (propietat
|
// Event pub/sub minimalista. Hi ha un únic `EventBus` per mundus (propietat
|
||||||
// de l'`EntityManager`); les entitats que el necessiten en reben un punter
|
// de l'`EntityManager`); les entitats que el necessiten en reben un punter
|
||||||
// via `Entity::setEventBus`. Aquest sistema cobreix les necessitats
|
// via `Entity::setEventBus`. Aquest sistema cobreix les necessitats
|
||||||
// actuals (sarcophagus_opened, level_complete, player_hit) sense portar-hi
|
// actuals (sarcophagus_opened, level_complete, player_hit, on_tile_entered)
|
||||||
// senyalització més pesada.
|
// sense portar-hi senyalització més pesada.
|
||||||
//
|
//
|
||||||
// Decisió: `std::variant` per al payload — evita any/void* i facilita
|
// Decisió: `std::variant` per al payload — evita any/void* i facilita
|
||||||
// consumir events tipats amb `std::get_if`. Si apareixen més tipus de
|
// consumir events tipats amb `std::get_if`. Si apareixen més tipus de
|
||||||
// payload, s'amplia aquí sense tocar cridants existents.
|
// payload, s'amplia aquí sense tocar cridants existents.
|
||||||
|
|
||||||
|
// Tile (col, row) — payload de `on_tile_entered`. No és un `pair<int,int>`
|
||||||
|
// per evitar alias ambigus al variant (dos ints sense nom).
|
||||||
|
struct TilePos {
|
||||||
|
int col{0};
|
||||||
|
int row{0};
|
||||||
|
};
|
||||||
|
|
||||||
struct Event {
|
struct Event {
|
||||||
std::string name;
|
std::string name;
|
||||||
using Payload = std::variant<std::monostate, int, std::string, Entity*>;
|
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos>;
|
||||||
Payload payload;
|
Payload payload;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,6 +68,9 @@ class EventBus {
|
|||||||
void publish(const std::string& name, int value) {
|
void publish(const std::string& name, int value) {
|
||||||
publish(Event{.name = name, .payload = value});
|
publish(Event{.name = name, .payload = value});
|
||||||
}
|
}
|
||||||
|
void publish(const std::string& name, TilePos tile) {
|
||||||
|
publish(Event{.name = name, .payload = tile});
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Subscription {
|
struct Subscription {
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "game/entities/event_bus.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
#include "utils/defaults.hpp"
|
#include "utils/defaults.hpp"
|
||||||
|
|
||||||
@@ -106,6 +108,34 @@ void Player::update(float delta_time) {
|
|||||||
if (Map::collides(baseBox())) { setPosY(OLD_Y); }
|
if (Map::collides(baseBox())) { setPosY(OLD_Y); }
|
||||||
|
|
||||||
clampToPlayfield();
|
clampToPlayfield();
|
||||||
|
|
||||||
|
// Detecció de canvi de tile. Es calcula *després* del clamp i de les
|
||||||
|
// col·lisions perquè la posició aquí ja és la definitiva del frame;
|
||||||
|
// així els sarcòfags no reben falsos positius mentre el Player rellisca
|
||||||
|
// contra una paret. Si el tile ha canviat, publiquem `on_tile_entered`
|
||||||
|
// al bus; l'EntityManager farà fan-out a notifyTileEntered.
|
||||||
|
if (bus_ != nullptr) {
|
||||||
|
const auto [COL, ROW] = getCurrentTile();
|
||||||
|
if (COL != last_tile_col_ || ROW != last_tile_row_) {
|
||||||
|
last_tile_col_ = COL;
|
||||||
|
last_tile_row_ = ROW;
|
||||||
|
bus_->publish("on_tile_entered", TilePos{.col = COL, .row = ROW});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Player::getCurrentTile() const -> std::pair<int, int> {
|
||||||
|
// El grid del tilemap és de `Map::tile_size` píxels i està 16-alineat
|
||||||
|
// respecte a l'origen del tilemap (pot ser negatiu per overscan). El
|
||||||
|
// centre de la caixa de col·lisió ens dóna el tile on "està" el Player.
|
||||||
|
const auto BOX = baseBox();
|
||||||
|
const float CX = BOX.x + (BOX.w * 0.5F);
|
||||||
|
const float CY = BOX.y + (BOX.h * 0.5F);
|
||||||
|
const int COL = static_cast<int>(std::floor(
|
||||||
|
(CX - static_cast<float>(Map::tilemap_origin_x)) / static_cast<float>(Map::tile_size)));
|
||||||
|
const int ROW = static_cast<int>(std::floor(
|
||||||
|
(CY - static_cast<float>(Map::tilemap_origin_y)) / static_cast<float>(Map::tile_size)));
|
||||||
|
return {COL, ROW};
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::clampToPlayfield() {
|
void Player::clampToPlayfield() {
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||||
|
|
||||||
|
class EventBus;
|
||||||
|
|
||||||
class Player : public AnimatedSprite {
|
class Player : public AnimatedSprite {
|
||||||
public:
|
public:
|
||||||
// Mapeig de tecles per al moviment del jugador. Cada jugador local en pot
|
// Mapeig de tecles per al moviment del jugador. Cada jugador local en pot
|
||||||
@@ -47,6 +50,16 @@ class Player : public AnimatedSprite {
|
|||||||
return getPosY() + static_cast<float>(collision_y_offset + collision_size);
|
return getPosY() + static_cast<float>(collision_y_offset + collision_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Injecta el bus del nivell. El Player publica `on_tile_entered` cada
|
||||||
|
// vegada que el centre de la seua caixa de col·lisió entra en una
|
||||||
|
// cel·la nova del tilemap. Si el bus no s'injecta, no emet res.
|
||||||
|
void setEventBus(EventBus* bus) { bus_ = bus; }
|
||||||
|
|
||||||
|
// Tile actual del jugador (centre de `baseBox()` convertit a
|
||||||
|
// coordenades de tile del tilemap, usant Map::tile_size i
|
||||||
|
// Map::tilemap_origin_*). Retorna (col, row).
|
||||||
|
[[nodiscard]] auto getCurrentTile() const -> std::pair<int, int>;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Rectangle de col·lisió del jugador, usat pel mapa de colisions i com
|
// Rectangle de col·lisió del jugador, usat pel mapa de colisions i com
|
||||||
// a referència per al Z-order.
|
// a referència per al Z-order.
|
||||||
@@ -72,4 +85,13 @@ class Player : public AnimatedSprite {
|
|||||||
void (Player::*set_pos)(float));
|
void (Player::*set_pos)(float));
|
||||||
|
|
||||||
Controls controls_;
|
Controls controls_;
|
||||||
|
|
||||||
|
// Bus del nivell (opcional). Propietat externa (EntityManager); el
|
||||||
|
// Player només en guarda un punter per publicar events.
|
||||||
|
EventBus* bus_{nullptr};
|
||||||
|
|
||||||
|
// Últim tile publicat. Inicialitzats a un sentinel fora de rang perquè
|
||||||
|
// el primer frame en què el bus estigui connectat ja emeti un event.
|
||||||
|
int last_tile_col_{-9999};
|
||||||
|
int last_tile_row_{-9999};
|
||||||
};
|
};
|
||||||
@@ -41,6 +41,21 @@ namespace Map {
|
|||||||
back_wall_h = g["back_wall_h"].get_value<int>();
|
back_wall_h = g["back_wall_h"].get_value<int>();
|
||||||
side_wall_w = g["side_wall_w"].get_value<int>();
|
side_wall_w = g["side_wall_w"].get_value<int>();
|
||||||
|
|
||||||
|
// Tile size + origen del tilemap (per a consumidors externs que
|
||||||
|
// converteixen px ↔ tile). Fallbacks conservadors: tile_size per
|
||||||
|
// defecte 16, origen [0, 0]. Si falten del YAML, només el sistema
|
||||||
|
// d'entitats es veu afectat; la lògica de col·lisions actual no.
|
||||||
|
tile_size = yaml.contains("tile_size") ? yaml["tile_size"].get_value<int>() : 16;
|
||||||
|
tilemap_origin_x = 0;
|
||||||
|
tilemap_origin_y = 0;
|
||||||
|
if (yaml.contains("origin")) {
|
||||||
|
const auto& o = yaml["origin"];
|
||||||
|
if (o.is_sequence() && o.size() == 2) {
|
||||||
|
tilemap_origin_x = o[0].get_value<int>();
|
||||||
|
tilemap_origin_y = o[1].get_value<int>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Derivats: el passadís és el sobrant del pitch sobre el sarcòfag, i
|
// Derivats: el passadís és el sobrant del pitch sobre el sarcòfag, i
|
||||||
// l'exterior reutilitza la mateixa amplada per mantenir el pas lliure.
|
// l'exterior reutilitza la mateixa amplada per mantenir el pas lliure.
|
||||||
corridor_w = pitch_x - sarc_w;
|
corridor_w = pitch_x - sarc_w;
|
||||||
|
|||||||
@@ -24,6 +24,16 @@ namespace Map {
|
|||||||
inline int pitch_x = 0;
|
inline int pitch_x = 0;
|
||||||
inline int pitch_y = 0;
|
inline int pitch_y = 0;
|
||||||
|
|
||||||
|
// Metadades del tilemap (carregades de l'arrel del YAML: `tile_size:` i
|
||||||
|
// `origin: [x, y]`). Són necessàries per a consumidors externs com el
|
||||||
|
// sistema d'entitats, que tradueix coordenades píxel ↔ tile amb
|
||||||
|
// tile_col = floor((px - tilemap_origin_x) / tile_size)
|
||||||
|
// El grid és 16-alineat respecte a l'origen del tilemap (no del canvas);
|
||||||
|
// origen habitual a pyramid.yaml és [-8, 0] (overscan horitzontal CPC).
|
||||||
|
inline int tile_size = 0;
|
||||||
|
inline int tilemap_origin_x = 0;
|
||||||
|
inline int tilemap_origin_y = 0;
|
||||||
|
|
||||||
// Cromat de la sala: marcador (HUD), mur posterior (dalt) i murs
|
// Cromat de la sala: marcador (HUD), mur posterior (dalt) i murs
|
||||||
// laterals. El mur frontal es deriva del passadís exterior inferior.
|
// laterals. El mur frontal es deriva del passadís exterior inferior.
|
||||||
inline int hud_h = 0;
|
inline int hud_h = 0;
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <variant>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
@@ -15,6 +17,7 @@
|
|||||||
#include "core/rendering/render_info.hpp"
|
#include "core/rendering/render_info.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
|
#include "game/entities/event_bus.hpp"
|
||||||
#include "game/entities/player.hpp"
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
@@ -59,6 +62,23 @@ GameScene::GameScene(Config config)
|
|||||||
// Player perquè el spawn consulta `collision_*`.
|
// Player perquè el spawn consulta `collision_*`.
|
||||||
Player::loadFrom(config_.player_yaml);
|
Player::loadFrom(config_.player_yaml);
|
||||||
|
|
||||||
|
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
||||||
|
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
||||||
|
// `makeTilemap`), perquè consulta `Map::tile_size` i `getSarcRect`.
|
||||||
|
// Les surfaces de les entitats es carreguen al Resource::Cache aquí.
|
||||||
|
entity_manager_.loadFromLevel(config_.map_yaml);
|
||||||
|
|
||||||
|
// Subscripcions bàsiques al bus del nivell: de moment només logs, sense
|
||||||
|
// tocar ni la vida del jugador ni l'estat de la escena. Serà la base
|
||||||
|
// sobre la qual cablejar HUD i fi de nivell més endavant.
|
||||||
|
entity_manager_.getBus().subscribe("level_complete", [](const Event& /*ev*/) {
|
||||||
|
std::cout << "[LEVEL COMPLETE]" << '\n';
|
||||||
|
});
|
||||||
|
entity_manager_.getBus().subscribe("player_hit", [](const Event& ev) {
|
||||||
|
const auto* dmg = std::get_if<int>(&ev.payload);
|
||||||
|
std::cout << "[PLAYER HIT] dmg=" << (dmg != nullptr ? *dmg : 0) << '\n';
|
||||||
|
});
|
||||||
|
|
||||||
// Jugador 0: fletxes del teclat (Defaults::KeysGame). Instanciar-ne un
|
// Jugador 0: fletxes del teclat (Defaults::KeysGame). Instanciar-ne un
|
||||||
// segon només requereix afegir una altra entrada amb uns Controls
|
// segon només requereix afegir una altra entrada amb uns Controls
|
||||||
// distints (p.ex. WASD).
|
// distints (p.ex. WASD).
|
||||||
@@ -79,6 +99,10 @@ GameScene::GameScene(Config config)
|
|||||||
const auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F);
|
const auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F);
|
||||||
players_.emplace_back(std::make_unique<Player>(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1));
|
players_.emplace_back(std::make_unique<Player>(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1));
|
||||||
|
|
||||||
|
// Connectem el Player al bus perquè emeti `on_tile_entered` i els
|
||||||
|
// sarcòfags rebin les notificacions de pas per tile.
|
||||||
|
players_.back()->setEventBus(&entity_manager_.getBus());
|
||||||
|
|
||||||
// Música de fons en bucle infinit. El track ja està al Resource::Cache
|
// Música de fons en bucle infinit. El track ja està al Resource::Cache
|
||||||
// (preload al Director), així que playMusic només mou el punter cap a
|
// (preload al Director), així que playMusic només mou el punter cap a
|
||||||
// jail_audio i obri l'stream. Audio::get() està garantit no-null:
|
// jail_audio i obri l'stream. Audio::get() està garantit no-null:
|
||||||
@@ -94,6 +118,11 @@ void GameScene::update(int delta_ms) {
|
|||||||
|
|
||||||
const float DT = static_cast<float>(delta_ms) / 1000.0F;
|
const float DT = static_cast<float>(delta_ms) / 1000.0F;
|
||||||
for (auto& player : players_) { player->update(DT); }
|
for (auto& player : players_) { player->update(DT); }
|
||||||
|
// El tick d'entitats DESPRÉS del tick dels jugadors: si el jugador ha
|
||||||
|
// emès `on_tile_entered` aquest frame, l'EntityManager ja l'ha encaminat
|
||||||
|
// als sarcòfags via la subscripció del bus, i aquest `update` pot
|
||||||
|
// aplicar la transició OPENING → OPEN en el mateix frame.
|
||||||
|
entity_manager_.update(DT);
|
||||||
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW));
|
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW));
|
||||||
Notifier::get().update(delta_ms);
|
Notifier::get().update(delta_ms);
|
||||||
}
|
}
|
||||||
@@ -108,6 +137,11 @@ void GameScene::render() {
|
|||||||
// col·lisions del jugador.
|
// col·lisions del jugador.
|
||||||
tilemap_.render();
|
tilemap_.render();
|
||||||
|
|
||||||
|
// Entitats (sarcòfags, porta, momia, antorxa...) dibuixades per sobre
|
||||||
|
// dels tiles però sota dels players. Z-order fi (interleaving per feetY
|
||||||
|
// amb els sarcs) queda per a una tasca posterior; "sin pulido".
|
||||||
|
entity_manager_.renderAll();
|
||||||
|
|
||||||
// Amb un sol jugador, l'ordre és irrellevant i ens estalviem el vector
|
// Amb un sol jugador, l'ordre és irrellevant i ens estalviem el vector
|
||||||
// temporal. Quan n'hi haja més d'un (o quan els sarcs siguen sprites)
|
// temporal. Quan n'hi haja més d'un (o quan els sarcs siguen sprites)
|
||||||
// caldrà ordenar per `getFeetY()` ascendent i interleaveing amb les
|
// caldrà ordenar per `getFeetY()` ascendent i interleaveing amb les
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "core/scene/scene.hpp"
|
#include "core/scene/scene.hpp"
|
||||||
|
#include "game/entities/entity_manager.hpp"
|
||||||
#include "game/map/tilemap.hpp"
|
#include "game/map/tilemap.hpp"
|
||||||
|
|
||||||
class Player;
|
class Player;
|
||||||
@@ -33,6 +34,11 @@ class GameScene : public Scene {
|
|||||||
Config config_;
|
Config config_;
|
||||||
Tilemap tilemap_;
|
Tilemap tilemap_;
|
||||||
|
|
||||||
|
// Gestor d'entitats del nivell (sarcòfags, porta, momia, antorxa,
|
||||||
|
// etc.). Viu aquí perquè la seua vida coincideix amb la de la escena;
|
||||||
|
// cada entitat del nivell es carrega al ctor via loadFromLevel.
|
||||||
|
EntityManager entity_manager_;
|
||||||
|
|
||||||
// Vector des del principi: AEE suportarà multijugador local; cada Player
|
// Vector des del principi: AEE suportarà multijugador local; cada Player
|
||||||
// té els seus propis Controls i entra al vector amb una crida extra.
|
// té els seus propis Controls i entra al vector amb una crida extra.
|
||||||
std::vector<std::unique_ptr<Player>> players_;
|
std::vector<std::unique_ptr<Player>> players_;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
#include "game/entities/entity_loader.hpp"
|
#include "game/entities/entity_loader.hpp"
|
||||||
#include "game/entities/entity_manager.hpp"
|
#include "game/entities/entity_manager.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
|
#include "game/map/map.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -259,6 +260,62 @@ namespace {
|
|||||||
REQUIRE(!torch.interactable());
|
REQUIRE(!torch.interactable());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void testLoadFromLevel() {
|
||||||
|
std::cout << "[T9] pyramid level load (globals + spawns + tile math + bus)\n";
|
||||||
|
// 1) Map::loadFrom carrega les tres globals noves (tile_size, origen).
|
||||||
|
Map::loadFrom("data/maps/pyramid.yaml");
|
||||||
|
REQUIRE(Map::tile_size == 16);
|
||||||
|
REQUIRE(Map::tilemap_origin_x == -8);
|
||||||
|
REQUIRE(Map::tilemap_origin_y == 0);
|
||||||
|
|
||||||
|
// 2) El pyramid.yaml conté 5 spawns amb els tipus esperats.
|
||||||
|
const auto SPAWNS = EntityLoader::readSpawns("data/maps/pyramid.yaml");
|
||||||
|
REQUIRE(SPAWNS.size() == 5);
|
||||||
|
int torches = 0;
|
||||||
|
int sarcs = 0;
|
||||||
|
int doors = 0;
|
||||||
|
int mummies = 0;
|
||||||
|
for (const auto& s : SPAWNS) {
|
||||||
|
const auto T = s["type"].get_value<std::string>();
|
||||||
|
if (T == "torch") { ++torches; }
|
||||||
|
else if (T == "sarcophagus") { ++sarcs; }
|
||||||
|
else if (T == "door") { ++doors; }
|
||||||
|
else if (T == "mummy") { ++mummies; }
|
||||||
|
}
|
||||||
|
REQUIRE(torches == 1);
|
||||||
|
REQUIRE(sarcs == 2);
|
||||||
|
REQUIRE(doors == 1);
|
||||||
|
REQUIRE(mummies == 1);
|
||||||
|
|
||||||
|
// 3) La conversió sarc-grid (0,0) → tile anchor (3,4) (i.e. la
|
||||||
|
// fórmula que usa loadFromLevel). Sarc rect = (40, 64, 48, 16).
|
||||||
|
const auto RECT = Map::getSarcRect(0, 0);
|
||||||
|
REQUIRE(static_cast<int>(RECT.x) == 40);
|
||||||
|
REQUIRE(static_cast<int>(RECT.y) == 64);
|
||||||
|
const int ANCHOR_COL = (static_cast<int>(RECT.x) - Map::tilemap_origin_x) / Map::tile_size;
|
||||||
|
const int ANCHOR_ROW = (static_cast<int>(RECT.y) - Map::tilemap_origin_y) / Map::tile_size;
|
||||||
|
REQUIRE(ANCHOR_COL == 3);
|
||||||
|
REQUIRE(ANCHOR_ROW == 4);
|
||||||
|
|
||||||
|
// 4) Fan-out del bus: un sarcòfag ancorat a (3,4) mida 3×1 rep via
|
||||||
|
// on_tile_entered una visita sobre el seu perímetre → state canvia.
|
||||||
|
EntityManager manager;
|
||||||
|
auto sarc = std::make_unique<Sarcophagus>(EntityLoader::loadTemplate("sarcophagus"));
|
||||||
|
sarc->place(3, 4, {});
|
||||||
|
Sarcophagus* sarc_raw = manager.addSarcophagus(std::move(sarc));
|
||||||
|
REQUIRE(sarc_raw->getPerimeterSize() == 8);
|
||||||
|
// Les 8 tiles del perímetre: N (3..5,3), S (3..5,5), O (2,4), E (6,4).
|
||||||
|
const std::pair<int, int> PERIM[] = {
|
||||||
|
{3, 3}, {4, 3}, {5, 3},
|
||||||
|
{3, 5}, {4, 5}, {5, 5},
|
||||||
|
{2, 4}, {6, 4},
|
||||||
|
};
|
||||||
|
for (const auto& [c, r] : PERIM) {
|
||||||
|
manager.getBus().publish("on_tile_entered", TilePos{.col = c, .row = r});
|
||||||
|
}
|
||||||
|
REQUIRE(sarc_raw->getState() == Sarcophagus::State::OPENING);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto main() -> int {
|
auto main() -> int {
|
||||||
@@ -272,6 +329,7 @@ auto main() -> int {
|
|||||||
testAnimationComponent();
|
testAnimationComponent();
|
||||||
testAllTemplatesLoad();
|
testAllTemplatesLoad();
|
||||||
testDecorationFlags();
|
testDecorationFlags();
|
||||||
|
testLoadFromLevel();
|
||||||
|
|
||||||
if (g_failures == 0) {
|
if (g_failures == 0) {
|
||||||
std::cout << "ALL OK\n";
|
std::cout << "ALL OK\n";
|
||||||
|
|||||||
Reference in new issue
Block a user