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

28 KiB
Raw Permalink 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.

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_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: 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 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: 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 Nou struct PaintedTile { col, row, tile_index } al Payload variant + overload publish.
map.hpp / .cpp Map::isPaintable(col, row) → true si el tile cau dins del playfield i no s'encavalca cap sarc.
map/paint_layer.hpp / .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 / .cpp Camp paint_tile_index_ al ctor (default -1). El Player publica PaintedTile amb el seu índex en lloc de TilePos.
entity.hpp / .cpp Rename notifyTileEntered → notifyTilePainted.
concrete/sarcophagus.hpp / .cpp Mateix rename. La lògica del visited_ és idèntica — només canvia qui li truca el hook.
entity_manager.hpp / .cpp La subscripció canvia d'on_tile_entered → tile_painted. Fan-out renombrat a notifyPlayerTilePainted.
game_scene.hpp / .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 Camps nous player1_paint_tile: 2 i player2_paint_tile: 2 (índex provisional, canviable sense recompilar).
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_2026: 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.