Refactor del sistema d'entitats per substituir filtres i
dynamic_casts dispersos per dispatch polimòrfic.
Quatre virtuals nous a Entity (`onWhipHit`, `onJustSpawned`,
`onRemoved`, `shouldReceiveContact`) encapsulen dins de cada
subclasse les decisions que abans vivien al manager:
- WhipAttack::applyDamage ja no fa `dynamic_cast<Mummy*>`.
- spawnPendingActors/spawnActor ja no fan `dynamic_cast<DynamicHazard*>`.
- removeEntity ja no compara `getName() == "sarcophagus"`.
- checkPlayerCollisions filtra amb un sol predicat virtual.
Amb el polimorfisme madurat, el sac dual `map_entities_`/`actors_`
introduit a la rama anterior ja no aporta valor: tornem a un sol
`entities_` (manté `pending_actors_` per al spawn diferit). El
manager itera un sol vector i cada entitat decideix què fa.
Cap dynamic_cast nou per a lògica de joc — només queden els
inevitables (validació de factory i ruteig de la Door).
discriminació
Reverteix la separació en sacs introduïda al commit 62f4b85 (rama
anterior). Amb els virtuals afegits al commit precedent
(`onWhipHit`, `onJustSpawned`, `shouldReceiveContact`, `onRemoved`),
ja no cal separar `map_entities_` i `actors_`: cada entitat decideix
internament què fa, el manager itera un sol vector i confia en el
dispatch virtual.
Canvis:
- `entities_` torna a ser el contenidor únic. Es desfà el dispatch
`dynamic_cast<DynamicHazard*>` que `add()` feia per enrutar.
- `addSarcophagus`, `removeEntity`, `update`, `notifyPlayerTilePainted`,
`findFloorTriggerAt`, `renderAll`, `reload` simplificats: un sol
bucle, sense distingir categories.
- `checkPlayerCollisions` només recorre `entities_` cridant
`shouldReceiveContact` polimòrfic — la lògica de "saltar SPAWN"
viu a `DynamicHazard::shouldReceiveContact`.
- `getMapEntities()` i `getActors()` desapareixen. Es manté
`getEntities()` com a accessor únic.
- `WhipAttack::applyDamage` itera `getEntities()` cridant `onWhipHit`
polimòrfic. Les entitats no-whippables retornen false (cost: una
crida virtual buida) i el callsite no discrimina tipus.
- `GameScene::render` mergeja entities + players (dos transforms en
comptes de tres) per al z-order.
`pending_actors_` es manté: és un concepte ortogonal — entitats que
encara no s'han instanciat al món fins que GameScene les "spawneja"
al text-exit de ROOM_INTRO.
Beneficis del nou disseny:
- Cap dynamic_cast dispers per a la lògica de joc (només queden els
inevitables: validació de factory i ruteig de la Door).
- Afegir un nou enemic implica `class X : public DynamicHazard` i
override dels virtuals rellevants — res al manager.
- Si un sarc, antorxa, o decoració futura volgués reaccionar al
fueteig (trencar-se, encendre's), basta amb un `onWhipHit` override
i no cal tocar `WhipAttack::applyDamage`.
Quatre virtuals nous a `Entity` capten decisions que abans vivien al
manager com a discriminacions de tipus:
- `onWhipHit(WhipAttack*)` → bool. Default no-op/false. Override a
`Mummy` per entrar HURT si és whippable. WhipAttack::applyDamage
passa de `dynamic_cast<Mummy*>` + `isWhippable()` a una crida
polimòrfica neta. RollingRock no override → immune al látigo per
disseny, sense flag.
- `onJustSpawned()`. Default no-op. Override a `DynamicHazard` per
disparar `triggerSpawnExit()`. `spawnPendingActors` i `spawnActor`
passen de `dynamic_cast<DynamicHazard*>(observer)->triggerSpawnExit()`
a `observer->onJustSpawned()`.
- `onRemoved()`. Default no-op. Override a `Sarcophagus` per cridar
`entity_manager_->onSarcophagusRemoved()`. `removeEntity` passa de
`getName() == "sarcophagus"` a dispatch polimòrfic.
- `shouldReceiveContact()`. Default `collides() || triggersOnOverlap()`.
Override a `DynamicHazard` per excloure SPAWN. `checkPlayerCollisions`
passa de tres condicions inline per a sacs distints a una crida
unificada.
`whipDirToMummyDir` es mou de whip_attack.cpp a mummy.cpp — el mapping
viu on s'usa (dins de `Mummy::onWhipHit`).
`hitMummyIfApplicable` es renomena a `hitEntityIfApplicable`: ja no
discrimina tipus, aplica `entity->onWhipHit` i marca `already_hit_`
segons el retorn.
Comportament idèntic, tests verds. Aquest commit prepara el següent:
amb el polimorfisme madurat, el sac dual ja no aporta valor i podem
tornar a un únic `entities_`.
Spawn on-demand de DynamicHazards via dos sacs a EntityManager:
les mòmies (i futurs hazards mòbils) ara s'instancien just quan
GameScene les "spawneja", no abans. Cinemàtica ROOM_INTRO neta:
mapa buit durant el text, mòmies surten emergint al text-exit
(fase 4, durada 1.0 s), jugadors cauen del cel a continuació.
Estat SPAWN/ACTIVE a DynamicHazard reutilitzable per a futurs
generadors (gotes, roques, etc.) via `EntityManager::spawnActor`.
Refactor col·lateral: EntityManager separa `map_entities_` /
`actors_` / `pending_actors_`. Render mergeja els tres orígens
(amb players). dynamic_casts dispersos a WhipAttack i col·lisió
substituïts per iteracions sobre el sac d'actors.
Bonus: arregla 4 tests latents de test_whip_attack que mai no
havien passat (timing off-by-one al primer update).
El YAML del whip declara frame_ms=80, així que el frame 0 viu [0, 80] ms
i `active_frames=[1, 2]` comença a 80 ms. Els tests feien `update(0.06F)`
amb el comentari "60 ms → frame 1 (active)" — incorrecte: 60 < 80, encara
al frame 0, sense danyar res. Tres asserts d'`getState() == HURT` mai
no es complien.
Canvi a `update(0.09F)`: assegura que el primer tick entra al frame 1
i `applyDamage()` corre. Comentari actualitzat amb el càlcul correcte.
Tres canvis coordinats que tanquen la cinemàtica desitjada de
ROOM_INTRO:
1. `spawn.duration_s` de mummy.yaml passa de 0.6 a 1.0 s. Les mòmies
tarden ~1 segon a "emergir" — temps suficient perquè el jugador
noti que estan apareixent, no que ja eren a la sala.
2. El disparador es mou de fase 5 (music_tail) a fase 4 (text-exit).
Ara, just quan el text "PIRÀMIDE X, SALA Y" comença a sortir de
pantalla, GameScene crida `entity_manager_.spawnPendingActors()`
i les mòmies s'instancien ja en EMERGING. Visualment apareixen
alhora que el text desapareix; durant la fase 5 acaben d'emergir
i a l'entrada a PLAYING ja són ACTIVE — coincidint amb la caiguda
aèria dels jugadors.
3. `completeIntro` afegeix un fallback: si arribem a PLAYING sense
haver passat per fase 4 (config patològica, fases molt curtes),
instanciem els pending igualment per garantir que la sala mai no
queda sense enemics.
Renomenat: `hazards_spawn_triggered_` → `pending_actors_spawned_`,
nom que reflecteix què representa el flag al model nou.
El mètode privat `triggerHazardSpawnExit()` desapareix: era un
wrapper que ja només cridava `spawnPendingActors()`. Crida directa
des de `updateRoomTransition`, `respawnPlayersInstant` i
`completeIntro` (fallback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`loadFromLevel` ja no instancia immediatament les DynamicHazards del
YAML del mapa: les acumula a `pending_actors_` (estructura {type,
spawn_node}). El loader decideix per la categoria semàntica de la
plantilla (`type:` del YAML d'entity: "dynamic_hazard" → pending; la
resta → addSpawn d'instant).
Dues APIs noves consumeixen pending_actors_:
- `spawnPendingActors()` — itera la cua, instancia tots via addSpawn
i arma `triggerSpawnExit()` a cada un. Vacia la cua. El path
d'aparició massiva al final del text-exit de ROOM_INTRO.
- `spawnActor(type, col, row)` — instancia un únic actor on-demand
des d'un spawn YAML sintètic. Pensat per a futurs generadors
(StaticHazards que produeixen DynamicHazards en runtime: gotes,
roques, etc.). Cada actor neix ja en EMERGING.
`GameScene::triggerHazardSpawnExit` ara és un wrapper de
`spawnPendingActors()` — el dispar segueix en la subfase actual
(fase 5 de ROOM_INTRO) i a `respawnPlayersInstant`. El canvi de timing
ve en el commit següent.
`reload` també buida pending_actors_. Log: "N map entities + M actors
pending des de PATH" — visible per a diagnòstic.
Sense canvis de comportament observables al joc: les mòmies segueixen
apareixent al final del text-exit, però ara la "aparició" és la seua
instanciació real (abans ja existien quietes des del fade-in).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EntityManager guanya dues llistes lògiques:
- `map_entities_` — decoració, static_hazards (Hole, FloorTrap, WallTrap)
i interactables (Sarcophagus, Door). Persistents tota la sala.
- `actors_` — DynamicHazards (Mummy, RollingRock, futurs enemics).
Tipats com `unique_ptr<DynamicHazard>`, sense dynamic_cast.
`add()` enruta automàticament segons sigui DynamicHazard o no.
`update`, `notifyPlayerTilePainted`, `findFloorTriggerAt`, `removeEntity`,
`reload` i `renderAll` iteren el(s) sac(s) que toca.
`checkPlayerCollisions`: el filtre de SPAWN sobre el sac d'actors ja no
necessita dynamic_cast — `actor->isSpawning()` és directe. El filtre per
floor decals només toca map_entities_ (els decals viuen exclusivament
allà). Filtres per-jugador extrets a helpers per acotar complexitat.
API nova: `getMapEntities()` i `getActors()` exposen cada sac per
separat. `getEntities()` es manté com a concatenació per retro-compat
(editor, z-sort debug).
`GameScene::triggerHazardSpawnExit` itera `getActors()` directe — fora
dynamic_cast.
`GameScene::render` mergeja els tres orígens (map + actors + players)
en un sol vector temporal abans del stable_sort per `feet_y` — mateix
patró que ja s'usava amb players, ara amb una transformació més.
Comportament idèntic: les momies segueixen instanciant-se al
loadFromLevel. La preparació per al spawn diferit ve en el commit
següent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GameScene ara crida `triggerHazardSpawnExit()` quan ROOM_INTRO entra a
la fase 5 (music_tail, immediatament després del text-exit): itera els
DynamicHazards de la sala i els arma el comptador `spawn.duration_s`.
Així la cinemàtica queda:
fade-in → text-enter → text-hold → text-exit
└─> mòmies surten "del terra" (durada `spawn.duration_s` del YAML)
└─> jugadors cauen del cel (delay player_spawn_delay_ms)
`respawnPlayersInstant` (salt debug a sala) també crida el trigger
perquè les mòmies no quedin congelades quan es curt-circuita la
cinemàtica.
Flag `hazards_spawn_triggered_` garanteix idempotència dins el cicle;
es reseteja a `enterRoomIntro` i al `setState(ROOM_INTRO)` defensiu.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
La mòmia ara entra a l'estat SPAWN al construir-se: visible al
pixel_rect_ inicial, sense moviment ni dany, esperant que algú extern
(GameScene) li armi el comptador via `triggerSpawnExit()`.
La durada (`spawn.duration_s` al YAML, default 0) controla quan passa
a ROAMING una vegada armat el comptador. Posat a 0.6s a mummy.yaml.
Aquest commit encara no enganxa el trigger: la mòmia es queda quieta
tota la partida — comportament visual intermedi, esperat. El commit
següent posarà el trigger a GameScene al final del text-exit de
ROOM_INTRO.
Tests de smoke i whip ajustats: les mòmies construïdes en stand-alone
necessiten `triggerSpawnExit()` + un update gran abans d'interactuar
amb `enterHurt`/contacto. Helper `exitMummySpawn` afegit a cada fitxer.
Guards defensius a `enterHurt` i `onPlayerContact`: encara que el filtre
central a EntityManager salta DynamicHazards en SPAWN, blindem la
crida directa per a futurs camins (whip propers, scripts...).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>