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

16 KiB
Raw Blame History

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 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 / .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.
  • 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. 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): 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, 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):
    • 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:

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.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.