Cause : this.selectedEventCard gardait une référence DIRECTE vers un objet
Phaser (ItemBoardToken), régulièrement détruit et recréé par
_refreshEventHandAffordability()/_buildEventHand() à chaque game-state.
Cette référence devenait alors obsolète (pointant vers un objet détruit),
et l'état de verrouillage "carte en cours de jeu" était géré par deux
chemins de mutation distincts (reconstruction de la main basée sur
myEventHand seul, ET synchronisation séparée sur pendingEventPlayId) qui
pouvaient se désynchroniser selon l'ordre exact des événements réseau :
une carte déjà envoyée au serveur (play-event émis) pouvait alors
réapparaître visuellement "jouable" dans la main après une reconstruction
intermédiaire qui ignorait le verrouillage réel.
Fix : refactor complet du pilotage de la main d'événements côté GameScene :
- this.selectedEventCard (référence Phaser) remplacé par
this.selectedEventId (simple eventId, jamais périmé même après
destruction/recréation des jetons).
- Fusion de _buildEventHand + _refreshEventHandAffordability en une seule
fonction _renderEventHand(eventIds), SEULE responsable de construire
this.eventCards — élimine le second chemin de mutation qui pouvait
diverger.
- Dans le listener 'game-state', selectedEventId est désormais TOUJOURS
resynchronisé sur state.pendingEventPlayId (source de vérité serveur)
AVANT de décider si la main doit être reconstruite, et la clé de diff
inclut maintenant ce verrouillage en plus de la composition de la main —
auparavant, un changement de pendingEventPlayId seul (main inchangée) ne
déclenchait aucune reconstruction, laissant l'ancien état affiché.
- _cancelTargeting() reconstruit désormais aussi la main pour lever tout
verrouillage visuel resté affiché après une annulation.
Fix connexe dans ItemBoardToken.destroy() : le minuteur d'OUVERTURE
(_showTimer) n'était jamais annulé à la destruction (seul _hideTimer
l'était) — un jeton détruit pendant qu'un survol venait de démarrer
pouvait déclencher _showExpanded() sur un objet déjà détruit une fois le
délai écoulé, une source de bug supplémentaire pour des cartes "fantômes".
54 tests unitaires client + 166 tests unitaires serveur toujours au vert.
ItemBoardToken._showExpanded() ancrait TOUJOURS la carte détaillée vers le
BAS du jeton compact, quel que soit son emplacement sur le plateau —
adapté aux boutiques d'objets/bâtiments (positionnées en haut du canvas),
mais pas à la main de cartes Événement (positionnée en BAS du canvas,
voir eventHandLayout.js) : la carte (jusqu'à 266px de haut avec le bouton
JOUER) débordait alors largement sous le bas de l'écran, la rendant
invisible et son bouton totalement injoignable au clic.
- Ajout du paramètre vAnchor ('bottom' par défaut, 'top' pour les jetons
proches du bas de l'écran) sur ItemBoardToken, symétrique au hAnchor déjà
existant pour l'axe horizontal. La main d'événements passe désormais
vAnchor='top' pour que la carte s'étende vers le HAUT au lieu du bas.
- Extraction du calcul de décalage (expandedCardOffset) et des constantes
de dimensionnement (EXPANDED_W/H, BUY_BTN_H) dans tokenLayout.js (module
pur, sans dépendance Phaser) pour pouvoir le couvrir par de vrais tests
unitaires Vitest.
- 11 nouveaux tests unitaires (tokenLayout.test.js) : orientation dx/dy
selon hAnchor/vAnchor, alignement exact du bord du jeton avec le bord de
la carte, et surtout la régression elle-même — un test vérifie que
vAnchor='top' maintient la carte dans les limites du canvas pour un
jeton en bas d'écran (1080p et 720p), et qu'un test symétrique confirme
que l'ancien comportement implicite (vAnchor='bottom') débordait bien
sous le canvas dans ce même scénario.
- 54 tests unitaires client au vert au total (43 précédents + 11 nouveaux),
vérification privilégiée par rapport à une nouvelle suite E2E (lente vu
la pioche aléatoire à stabiliser dans ce projet).
Deux problèmes de lisibilité/placement signalés :
1. Le texte des cartes Événement (nom + description) restait illisible
malgré le passage précédent de l'échelle 0.55 à 0.8 — le composant dédié
EventCardObject affichait une grande carte réduite, écrasant
systématiquement le texte quelle que soit l'échelle choisie.
Fix : suppression complète d'EventCardObject, remplacé par le même
pattern que les objets/bâtiments (ItemBoardToken, nouveau kind='event') :
un petit jeton compact (46x46) toujours visible sur le plateau, avec la
carte détaillée affichée en TAILLE RÉELLE (police 13-16px, jamais
réduite) au survol de la souris, et un bouton "JOUER" (au lieu
d'"ACHETER") pour confirmer. ItemBoardToken généralisé pour supporter ce
3e kind (pas de badge de coût affiché — déjà payé à la pioche — mais
badge "!" repris pour les événements à effet négatif type Épidémie).
Extraction de client/src/game/objects/tokenLayout.js (ICON_SIZE) dans un
module SANS dépendance à Phaser, pour que eventHandLayout.js (et ses
tests Vitest) puisse l'importer sans jamais charger Phaser — qui échoue
à l'import en environnement jsdom (canvas natif requis, absent de jsdom).
2. Les mini-cartes objets/bâtiments déjà achetés par un ADVERSAIRE étaient
affichées SOUS son slot de jeu, visuellement en dessous/après sa carte
jouée. Déplacées AU-DESSUS de son slot pour les mettre davantage en
évidence, comme posées sur sa carte plutôt que cachées dessous.
- 43 tests unitaires client mis à jour (eventHandLayout.test.js migré vers
ICON_SIZE au lieu de CARD_W/CARD_H/scale, désormais indépendant de Phaser).
- e2e/tests/helpers.js : playEventCard adapté au nouveau flux
survol->bouton JOUER (au lieu d'un simple clic direct sur la carte) ;
drawEventUntil corrigé pour ne jamais défausser une carte 'royal_accord'
sans fournir ses 2 cibles de ciblage séquentiel obligatoire (le jeu sans
cible bloquait silencieusement la séquence, sans jamais émettre
'play-event', rendant la boucle de pioche très peu fiable).
- Vérification ciblée via un test E2E existant (pas de nouvelle suite E2E
lourde, conformément à la demande de privilégier les tests unitaires) :
le nouveau flux jeton->survol->JOUER->résolution fonctionne bout en bout
(Récolte exceptionnelle +2 Nourriture confirmé après résolution).
Les cartes Événement (bouton de pioche + main) étaient positionnées trop
près du bouton "CONFIRMER MA CARTE" — le bouton de pioche chevauchait même
physiquement son bord droit de ~25px (x = width/2 + 210, même Y), au point
de prêter à confusion entre "piocher un événement" et "jouer une carte".
- Nouveau module pur client/src/game/objects/eventHandLayout.js
(eventDrawButtonPosition, eventHandGeometry, eventCardPosition) : ancre
toute la zone événement en bas-DROITE du canvas, clairement séparée du
bouton de confirmation (centré) et de la main de personnages (centrée).
- EVENT_CARD_SCALE remonté de 0.55 à 0.8 pour que le nom de l'événement
(police de base augmentée 12px -> 15px) reste réellement lisible une fois
réduit — ajout d'un bandeau semi-opaque derrière le texte (EventCardObject)
pour garantir le contraste quel que soit le portrait en arrière-plan.
- La légende des attributs occupait déjà le coin bas-droit : déplacée en
bas-GAUCHE (setOrigin ajusté en conséquence) pour laisser la place à la
nouvelle zone événement.
- 16 nouveaux tests unitaires Vitest (eventHandLayout.test.js) couvrant la
géométrie : positions dans les limites du canvas, absence de
chevauchement avec le bouton de confirmation/la main/la légende, ancrage
stable au bord droit avec 1 ou 2 cartes, robustesse sur petite résolution
(1280x720) et cardCount=0. Choix explicite de tests UNITAIRES (rapides,
~600ms pour toute la suite client) plutôt que E2E pour cette correction
de positionnement pur, les tests E2E de ce projet étant lents (plusieurs
minutes par scénario à cause de la pioche aléatoire à stabiliser).
Dernier point de la checklist de la fonctionnalité Cartes Événement
(pioche partagée, ciblage, synergies) resté en suspens. Ajout également des
règles des Bâtiments (Église, Château Royal), qui n'avaient jamais été
documentées lors de leur implémentation précédente. Mise à jour de
l'arborescence du projet dans la section Structure.
Trois bugs fonctionnels réels découverts et corrigés en stabilisant les
tests E2E des cartes Événement :
1. client/src/stores/game.js — le store Pinia n'exposait JAMAIS les champs
liés aux bâtiments (myBuildings, pendingBuyBuildingId, availableBuildings)
ni aux événements (myEventHand, eventDeckSize, hasPendingEventDraw,
pendingEventPlayId) : updateState() ne les lisait pas depuis le
'game-state' serveur. Conséquence concrète : GameScene._epidemicNeedsTarget()
lisait this.store.myBuildings (toujours vide) pour décider si la
variante Église d'Épidémie s'applique — le ciblage de la variante Trousse
ne fonctionnait donc jamais correctement dès qu'un joueur possédait
aussi le Château. Corrigé + 4 nouveaux tests unitaires Vitest.
2. client/src/game/scenes/GameScene.js — GameScene._onEventCardSelected
envoyait encore les clés historiques gainTargetSocketId/
loseTargetSocketId/targetSocketId au serveur, alors que Room.js avait été
migré vers gainTargetTeamIndex/loseTargetTeamIndex/targetTeamIndex
(socketId n'est jamais exposé au client). Conséquence : le ciblage
d'Accord royal et de la variante Trousse d'Épidémie était intégralement
cassé en jeu réel — les clés ne correspondant à rien côté serveur, aucune
cible n'était jamais appliquée, silencieusement.
3. e2e/tests/helpers.js — drawEventUntil abandonnait immédiatement dès que
la main d'événements atteignait 2 cartes sans contenir l'id recherché,
rendant les tests E2E d'événements extrêmement flaky (avec seulement 6
types d'événements et une main de 2, l'échec dès les 2 premières pioches
est fréquent, pas rare). Corrigé : joue désormais une carte pour libérer
un slot et continue à piocher jusqu'à maxDraws (20).
Stabilisation supplémentaire :
- Toutes les fonctions d'introspection Phaser dans helpers.js sécurisées
(optional chaining) contre une scène pas encore initialisée après un
changement de tour/navigation sous charge (plusieurs pages Playwright
simultanées).
- Timeouts des tests events.spec.js augmentés (150-240s) pour laisser le
temps aux boucles d'accumulation de jauge + pioche aléatoire de converger.
- playwright.config.js : retries=1 même en local, pour absorber la
variance résiduelle des tests basés sur un tirage aléatoire, tout en
gardant une détection fiable des vraies régressions (qui échoueraient de
façon reproductible sur les deux tentatives).
- events.spec.js : test 'Accord royal' fournit désormais les 2 cibles
requises par le ciblage séquentiel (gain + perte) au lieu d'une seule,
ce qui laissait la séquence de ciblage bloquée sans jamais émettre
'play-event'.
Résultat : 166 tests unitaires serveur + 28 tests unitaires client
toujours au vert ; suite events.spec.js (9 tests E2E) passe intégralement
(certains via retry probabiliste documenté, comportement attendu vu la
pioche aléatoire dans le deck partagé).
- Nouveau server/src/game/events.js : 6 événements (Récolte exceptionnelle,
Épidémie, Levée des troupes, Accord royal, Complot découvert, Grande fête
au château), deck PARTAGÉ de 18 cartes (3 exemplaires de chacun) mélangé
au démarrage de la partie.
- Room.js : buyEventDraw() (coût fixe 1 point dans chacune des 4 jauges,
effet immédiat contrairement aux objets/bâtiments — pas de conflit de
stock possible), playEventCard() (choix de jouer une carte de sa main,
max 1 par tour, avec ciblage optionnel), _resolvePendingEventPlays()
(résolution de TOUS les événements du tour AVANT le calcul des gains de
cartes personnage, défausse systématique, protection "Complot découvert"
pour soi-même uniquement). Les 6 effets incluent leurs synergies avec les
objets/bâtiments existants (Charrue, Trousse+Église pour Épidémie,
Trébuchet, Sceau Royal, Château Royal → pioche bonus).
- IMPORTANT : le ciblage de joueur (Accord royal, variante Trousse
d'Épidémie) transite par teamIndex (identifiant public) et non socketId
(jamais exposé au client via getPlayersInfo/getStateFor) — le serveur
résout teamIndex -> joueur réel au moment de la résolution.
- Nouvel event socket 'buy-event-draw' (effet immédiat) et 'play-event'
(résolution différée), eventResults inclus dans round-result.
- Client : events.js (miroir), EventCardObject.js (carte épurée façon
CardObject, badge rouge "!" pour les événements à effet négatif).
BootScene génère les textures plein cadre (méthode _generateClippedTexture
généralisée, réutilisée avec les personnages).
- GameScene : bouton de pioche à droite du bouton de confirmation (état
grisé selon jauges/main pleine/pioche déjà tentée), seconde main de cartes
événement (max 2) au-dessus de ce bouton, sélection d'une carte pour la
jouer avec séquence de ciblage interactive (surbrillance des slots
adversaires cliquables) pour Accord royal et la variante Trousse
d'Épidémie.
- 61 nouveaux tests unitaires serveur (events.js + Room.js : pioche, choix
de jouer, résolution des 6 effets avec/sans synergie, Complot découvert,
ordre événements-avant-personnages, deck partagé).
- Nouveaux helpers et tests d'intégration E2E Playwright (e2e/tests/events.spec.js)
pour la pioche, la limite de main à 2, et les 6 effets — certains
scénarios restent probabilistes (pioche aléatoire dans le deck partagé) et
peuvent nécessiter une stabilisation supplémentaire du timing.
- Nouveau server/src/game/buildings.js : 2 bâtiments, chacun couvrant 2
attributs, coût réparti sur les deux jauges, garantit +1 par tour sur
chaque attribut, avec bonus de synergie +1 si l'objet correspondant est
aussi possédé (Église+Trousse/Sceau, Château+Charrue/Trébuchet).
- Room.js : buyBuilding() (validation coût multi-jauges), résolution des
achats en attente avec la même priorité/stock unique que les objets,
_applyBuildingEffects() (garantie de base + synergie, jamais de recul par
rapport aux gains déjà boostés par un objet). getStateFor/getPlayersInfo
exposent buildings/myBuildings/availableBuildings/pendingBuyBuildingId.
- Nouvel event socket 'buy-building', boughtBuildings inclus dans round-result.
- Client : buildings.js (miroir), BootScene génère les textures portrait des
bâtiments (méthode _generatePortraitTexture généralisée, réutilisée avec
les objets). ItemBoardToken généralisé (kind='item'|'building', hAnchor
'left'|'right') pour afficher indifféremment un objet ou un bâtiment, avec
coût détaillé par jauge affiché sur la carte agrandie pour les bâtiments.
- GameScene : boutique de bâtiments en colonne à DROITE du plateau
(symétrique à la boutique d'objets à gauche), mes bâtiments possédés
affichés à GAUCHE de ma main (symétrique à mes objets à droite), bâtiments
des adversaires affichés sous leurs objets au niveau de leur slot.
- Fix : la carte détaillée héritait par défaut d'un ancrage "s'étend vers la
droite" adapté à la boutique d'objets (bord gauche du canvas) mais cassait
la boutique de bâtiments (bord droit) — le bouton ACHETER se retrouvait
hors du canvas et donc injoignable au clic. Ajout du paramètre hAnchor.
- 26 nouveaux tests unitaires serveur (buildings.js + Room.js : validations
d'achat multi-jauges, priorité/stock unique, effets de base et synergie).
- 6 nouveaux tests E2E Playwright (boutique bâtiments : affichage, position,
carte détaillée dans les limites du canvas, coût affiché, achat complet).
- Fix flakiness des tests E2E d'achat (item-shop + building-shop) : le mode
'min' de playCard garantit désormais que l'adversaire joue sa carte la
plus faible sur l'attribut ciblé, au lieu de simplement jouer un autre
attribut (insuffisant, le gain dépend de la comparaison au voisin gauche).
- Fix: UIScene n'était jamais lancée (active: false, jamais scene.launch)
donc l'event 'game-over' du serveur n'était jamais affiché — la partie
semblait ne pas avoir de fin. Remplacé par un panneau Vue dans GameView
(vainqueur, classement trié, bouton Quitter via vue-router). UIScene.js
supprimée (code mort).
- Extraction de client/src/utils/hudUtils.js (constantes ATTRS/couleurs et
gaugePercent) depuis GameView.vue pour les rendre testables isolément.
- Tests unitaires client (Vitest) : store Pinia (game.js) et hudUtils
(24 tests, client/test/unit/).
- Tests d'intégration E2E (Playwright, e2e/) avec de vrais clients
Socket.io et rendu Phaser réel dans Chromium :
* game-flow.spec.js : lobby → démarrage → main de 4 cartes → résolution
* item-shop.spec.js : boutique, bouton désactivé si jauge insuffisante,
achat retire l'objet pour tous + effet appliqué + affiché sur la table
* game-over.spec.js : partie complète (12 tours) → panneau de fin
webServer démarre automatiquement client+serveur ; ajout d'une route
GET /health côté Express pour permettre à Playwright de détecter un
serveur déjà démarré.
- README : documentation des 3 niveaux de tests (unitaires serveur,
unitaires client, intégration E2E) et de la nouvelle arborescence.
- Nouveaux personnages illustrés (12 PNG remplacés dans client/public/cards)
- 4 objets achetables (Trébuchet, Charrue, Trousse du Médecin, Sceau Royal)
avec effets permanents (double_gain, guaranteed_gain, copy_best_in_game),
stock unique par partie, priorité d'achat tournante entre joueurs
- Boutique en colonne à gauche du plateau : icônes compactes, carte détaillée
agrandie au survol (taille découplée du displayScale du jeton), bouton
ACHETER actif/grisé selon la jauge du joueur, retrait immédiat de la
colonne une fois l'objet acheté (les autres remontent pour combler)
- Objets achetés affichés "posés sur la table" : à côté de la main pour soi,
sous le slot de jeu pour les adversaires
- Icône du premier joueur (priorité d'achat) dans le HUD Vue
- Refonte des cartes personnage (CardObject) : portrait plein cadre épuré,
valeurs d'attribut discrètes aux 4 coins
- Fix removePlayer : nettoyage de pendingBuys/pendingPlays et recalage de
firstPlayerIndex à la déconnexion d'un joueur
- 77 tests unitaires serveur (node:test) : cards, items, Room, RoomManager
- README mis à jour avec les règles complètes du jeu
- .gitignore : exclusion de .idea/ et .serena/ (artefacts d'outils locaux)