fix: cartes Événement illisibles + objets/bâtiments adversaires au-dessus de leur carte

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).
This commit is contained in:
Kevin L'Hereec committed 2026-08-24 16:01:04 +02:00
1 parent 45eb9be60d
commit 1a91a0f876
7 files changed
+316 -309

No files matched your search

+32 -47
View File
@@ -2,10 +2,9 @@ import { describe, it, expect } from 'vitest'
import {
eventDrawButtonPosition,
eventHandGeometry,
eventCardPosition,
EVENT_CARD_SCALE
eventCardPosition
} from '../../src/game/objects/eventHandLayout'
import { CARD_W, CARD_H } from '../../src/game/objects/cardLayout'
import { ICON_SIZE } from '../../src/game/objects/tokenLayout'
// Dimensions de canvas typiques utilisées dans les tests (cohérentes avec
// GameFactory.js : width = innerWidth, height = innerHeight - 60).
@@ -13,13 +12,6 @@ const CANVAS_W = 1920
const CANVAS_H = 1080
describe('game/objects/eventHandLayout', () => {
describe('EVENT_CARD_SCALE', () => {
it('reste suffisamment proche de 1 pour garder le texte lisible (pas 0.55 comme l\'ancien placement)', () => {
expect(EVENT_CARD_SCALE).toBeGreaterThanOrEqual(0.7)
expect(EVENT_CARD_SCALE).toBeLessThanOrEqual(1)
})
})
describe('eventDrawButtonPosition', () => {
it('positionne le bouton dans le quart bas-droit du canvas', () => {
const { x, y } = eventDrawButtonPosition(CANVAS_W, CANVAS_H)
@@ -36,13 +28,12 @@ describe('game/objects/eventHandLayout', () => {
expect(y + buttonHeight / 2).toBeLessThanOrEqual(CANVAS_H)
})
it('est positionné strictement au-dessus de la main de cartes événement (pas de chevauchement)', () => {
it('est positionné strictement au-dessus de la main de jetons événement (pas de chevauchement)', () => {
const buttonHeight = 30
const { y: btnY } = eventDrawButtonPosition(CANVAS_W, CANVAS_H)
const { y: handY } = eventHandGeometry(CANVAS_W, CANVAS_H, 1)
const cardH = CARD_H * EVENT_CARD_SCALE
const buttonBottom = btnY + buttonHeight / 2
const handTop = handY - cardH / 2
const handTop = handY - ICON_SIZE / 2
expect(buttonBottom).toBeLessThanOrEqual(handTop)
})
@@ -74,22 +65,20 @@ describe('game/objects/eventHandLayout', () => {
expect(y).toBeGreaterThan(CANVAS_H / 2)
})
it('la dernière carte est ancrée sur le bord droit du canvas (avec la marge)', () => {
const { startX, spacing, scale } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
const cardW = CARD_W * scale
const lastCardCenterX = startX + spacing // index 1 = dernière carte pour cardCount=2
const lastCardRightEdge = lastCardCenterX + cardW / 2
expect(lastCardRightEdge).toBeLessThanOrEqual(CANVAS_W)
expect(lastCardRightEdge).toBeGreaterThan(CANVAS_W - 40) // proche du bord (marge ~20px)
it('le dernier jeton est ancré sur le bord droit du canvas (avec la marge)', () => {
const { startX, spacing } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
const lastCenterX = startX + spacing // index 1 = dernier jeton pour cardCount=2
const lastRightEdge = lastCenterX + ICON_SIZE / 2
expect(lastRightEdge).toBeLessThanOrEqual(CANVAS_W)
expect(lastRightEdge).toBeGreaterThan(CANVAS_W - 40) // proche du bord (marge ~20px)
})
it('avec 1 carte, elle est ancrée sur le bord droit (pas de décalage vers la gauche)', () => {
it('avec 1 jeton, il est ancré sur le bord droit (pas de décalage vers la gauche)', () => {
const { startX } = eventHandGeometry(CANVAS_W, CANVAS_H, 1)
const { startX: startXTwo } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
// La position de la DERNIÈRE carte doit être identique quel que soit
// le nombre de cartes (ancrage à droite stable) — avec 1 carte,
// startX doit correspondre à la position de la dernière carte du cas à 2.
const { spacing } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
const { startX: startXTwo, spacing } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
// La position du DERNIER jeton doit être identique quel que soit le
// nombre de jetons (ancrage à droite stable) — avec 1 jeton, startX
// doit correspondre à la position du dernier jeton du cas à 2.
expect(startX).toBeCloseTo(startXTwo + spacing, 5)
})
@@ -100,27 +89,24 @@ describe('game/objects/eventHandLayout', () => {
expect(Number.isFinite(spacing)).toBe(true)
})
it('l\'espacement entre cartes est toujours positif et couvre au moins la largeur d\'une carte', () => {
const { spacing, scale } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
expect(spacing).toBeGreaterThan(CARD_W * scale)
it('l\'espacement entre jetons est toujours positif et couvre au moins la largeur d\'un jeton', () => {
const { spacing } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
expect(spacing).toBeGreaterThan(ICON_SIZE)
})
it('les 2 cartes ne se chevauchent jamais (distance entre centres >= largeur d\'une carte)', () => {
const { spacing, scale } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
const cardW = CARD_W * scale
expect(spacing).toBeGreaterThanOrEqual(cardW)
it('les 2 jetons ne se chevauchent jamais (distance entre centres >= largeur d\'un jeton)', () => {
const { spacing } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
expect(spacing).toBeGreaterThanOrEqual(ICON_SIZE)
})
it('reste entièrement dans le canvas sur une petite résolution (1280x720)', () => {
const { startX, y, spacing, scale } = eventHandGeometry(1280, 720, 2)
const cardW = CARD_W * scale
const cardH = CARD_H * scale
const firstCardLeftEdge = startX - cardW / 2
const lastCardRightEdge = startX + spacing + cardW / 2
expect(firstCardLeftEdge).toBeGreaterThanOrEqual(0)
expect(lastCardRightEdge).toBeLessThanOrEqual(1280)
expect(y + cardH / 2).toBeLessThanOrEqual(720)
expect(y - cardH / 2).toBeGreaterThanOrEqual(0)
const { startX, y, spacing } = eventHandGeometry(1280, 720, 2)
const firstLeftEdge = startX - ICON_SIZE / 2
const lastRightEdge = startX + spacing + ICON_SIZE / 2
expect(firstLeftEdge).toBeGreaterThanOrEqual(0)
expect(lastRightEdge).toBeLessThanOrEqual(1280)
expect(y + ICON_SIZE / 2).toBeLessThanOrEqual(720)
expect(y - ICON_SIZE / 2).toBeGreaterThanOrEqual(0)
})
})
@@ -133,7 +119,7 @@ describe('game/objects/eventHandLayout', () => {
expect(pos1).toEqual({ x: startX + spacing, y })
})
it('les positions de 2 cartes sont distinctes', () => {
it('les positions de 2 jetons sont distinctes', () => {
const pos0 = eventCardPosition(CANVAS_W, CANVAS_H, 2, 0)
const pos1 = eventCardPosition(CANVAS_W, CANVAS_H, 2, 1)
expect(pos0.x).not.toBe(pos1.x)
@@ -142,13 +128,12 @@ describe('game/objects/eventHandLayout', () => {
describe('non-régression : séparation avec la légende d\'attributs (bas-gauche)', () => {
it('la zone événement (bas-droite) ne recouvre pas la zone légende (bas-gauche, x proche de 0)', () => {
const { startX, scale } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
const cardW = CARD_W * scale
const firstCardLeftEdge = startX - cardW / 2
const { startX } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
const firstLeftEdge = startX - ICON_SIZE / 2
// La légende occupe le coin bas-gauche (x ancré à 10px du bord gauche,
// largeur de texte ~180px max) — la zone événement doit rester dans
// la moitié droite du canvas pour ne jamais s'en approcher.
expect(firstCardLeftEdge).toBeGreaterThan(CANVAS_W / 2)
expect(firstLeftEdge).toBeGreaterThan(CANVAS_W / 2)
})
})
})