16 KiB
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; no hi havia entitats de joc (momies, antorxes, trampes, portes).
Objectiu inicial (prompt de partida):
- Classe base
Entityamb 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. Animationcom a component reutilitzable (composició, no herència).EntityFactory/EntityLoaderamb 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 / .cpp | Classe base; update, render, onPlayerContact(Player*), notifyTileEntered |
| source/game/entities/decoration.hpp | Header-only; collides=false, interactable=false |
| source/game/entities/interactable.hpp | Header-only; afegeix interact(Player*) |
| source/game/entities/hazard.hpp | Hazard + StaticHazard (cicle actiu/inactiu) + DynamicHazard (velocitat + step) |
| source/game/entities/animation_component.hpp / .cpp | Frames/fps/loop; tick, currentFrame. Completada d'inici si 1-frame no-loop |
| source/game/entities/event_bus.hpp / .cpp | Pub/sub amb std::variant payload |
| source/game/entities/entity_factory.hpp / .cpp | Macro REGISTER_ENTITY + registre estàtic per nom |
| source/game/entities/entity_loader.hpp / .cpp | Cache de plantilles YAML + readSpawns |
| source/game/entities/entity_manager.hpp / .cpp | Contenidor + comptador de sarcs + fan-out de tile |
| source/game/entities/concrete/torch.* | Decoració animada |
| source/game/entities/concrete/sarcophagus.* | Perímetre tiles ortogonal + estats |
| source/game/entities/concrete/door.* | Estats + open() + emissió level_complete |
| source/game/entities/concrete/mummy.* | chooseDirection virtual (aleatori per defecte) |
| source/game/entities/concrete/rolling_rock.* | Línia recta, direcció inicial del YAML |
| source/game/entities/concrete/wall_trap.* | StaticHazard amb cicle |
| source/game/entities/concrete/floor_trap.* | Igual que wall_trap, semàntica distinta |
| tools/entities/generate_placeholders.py | Genera 7 GIFs paletitzats amb Pillow |
data/entities/<name>/<name>.yaml + .gif |
7 plantilles d'entitat + 7 placeholders |
| data/maps/test_entities.yaml | Nivell mínim per al smoke |
| 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.Sarcophagusha 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 quanEntityManagerconeix la posició.- Registre estàtic via macro
REGISTER_ENTITY— perquè funcioni, tots elsconcrete/*.cppentren directament aAPP_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üentupdate.
Smoke tests fase 1
Target CMake test_entities_smoke al final de 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
Mapglobals noves (map.hpp:36-40):tile_size,tilemap_origin_x,tilemap_origin_y. Poblats aloadFromdes deyaml["tile_size"]iyaml["origin"].EventBuspayload ampliat ambstruct TilePos { int col; int row; }+ overloadpublish(name, TilePos).Entityambstd::shared_ptr<Surface>,setSurface/getSurface/setPixelRect. Per evitar arrossegarSurfaceal smoke,Entity::render()és no-op i el blit real el faEntityManager::renderAll, centralitzat.PlayerambsetEventBus(EventBus*)+getCurrentTile()(fórmula correcta ambtilemap_origin_*+tile_size). Al final d'update, si el tile canvia, publicaon_tile_enteredamb payloadTilePos.EntityManager::loadFromLevel(viu a entity_manager_loadlevel.cpp, TU separat per aïllar la dependència aResource::Cache):sarcophagus→sarc_position→Map::getSarcRect→ tile anchor per al perímetre.- resta →
position(tile coords del tilemap) → píxels viacol*tile_size + origin.
EntityManager::renderAll(al mateix TU). Blit directe deSurface::renderapixel_rect_.EntityManagersubscriuon_tile_enteredal ctor: quan repTilePos, fa fan-out vianotifyPlayerEnteredTile(col, row)a totes les entitats.GameScene(game_scene.cpp):- Membre
EntityManager entity_manager_. - Ctor:
loadFromLevel(config_.map_yaml)després de carregar mapa; subscripcionslevel_completeiplayer_hit(log astd::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()entretilemap_.render()i els Players.
- Membre
data/maps/pyramid.yamlamb 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:
ctor/dtor/add/setDoor/addSarcophagus/update/notifyPlayerEnteredTile— linkat al smoke. - 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 (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. |
DEFERRED |
| REND-02 | Animacions multi-frame. renderAll clipa el frame actual via src_rect calculat amb Entity::getAnimation()->currentFrame() sobre una graella left-to-right / top-to-bottom; columnes deduïdes de surface->getWidth() / pixel_rect.w. Así un strip 1×N, N×1 o N×M funciona igual. Torch primer consumidor real (4 frames @ 8 fps). |
DONE |
| REND-03 | Paleta compartida amb el tilemap. Cada GIF placeholder té la seua paleta pròpia (Pillow); funciona perquè Surface desa paleta per surface. Amb l'art real pot caldre unificar per evitar mismatch cromàtic. |
DEFERRED |
Perímetre i col·lisió
| ID | Descripció | Estat |
|---|---|---|
| PER-01 | blocked_predicate al Sarcophagus per descartar tiles ocupats per altres sarcs. Actualment loadFromLevel passa {}. Els spawns de demo eviten sarcs veïns; al primer disseny amb 4×5 sarcs adjacents caldrà connectar-ho (amb Map::collides traduït a tiles). |
DEFERRED |
| PER-02 | Perímetre diagonal opcional. Ara el perímetre és ortogonal (N/S/E/O). Si volem variants amb cantonades exigides, es pot afegir una flag al YAML de l'entitat. | WONTFIX (per ara — el comportament Oh Mummy! clàssic és ortogonal) |
Infraestructura
| ID | Descripció | Estat |
|---|---|---|
| INF-01 | Dead-code elimination del registre estàtic. Funciona mentre concrete/*.cpp viuen a APP_SOURCES. Si algun dia es modularitza a STATIC libs caldrà --whole-archive o un entity_registry_bootstrap.cpp amb cridades explícites. |
WONTFIX (vigilar) |
| INF-02 | Multiplayer: el bus del nivell és únic. Quan hi hagi múltiples Players, cal decidir si el on_tile_entered porta també el player_id. Ampliació trivial de TilePos o struct nou. |
DEFERRED |
| INF-03 | Integrar animacions multi-frame al YAML: els camps frames: [0] actuals són placeholders. Quan arribi l'art, el loader ja és tolerant — només cal omplir els vectors. |
WONTFIX (ja previst al codi) |
| INF-04 | Hook on_tile_entered emet cada frame que el tile canvia, no cada frame que el jugador es mou. Si algun dia es vol granularitat sub-tile (p.ex. "passar pel centre exacte"), caldrà un event separat. |
WONTFIX |
Tests
| ID | Descripció | Estat |
|---|---|---|
| TST-01 | Test d'integració amb loadFromLevel real: el smoke actual evita el Cache per no arrossegar dependències. Un test separat amb el Cache enllaçat podria verificar el blit, però demana instanciar Screen/Canvas. |
WONTFIX (l'smoke d'arrencada sota xvfb-run ja cobreix el camí viu) |
| TST-02 | Test de moviment de la momia (quan ENT-IA-01 estigui fet). Pendent juntament amb la IA. | DEFERRED |
6. Estat global
- Andamiatge i integració bàsica: DONE. Els 5 actors del
pyramid.yamles 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.