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).
140 lines
6.5 KiB
JavaScript
140 lines
6.5 KiB
JavaScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
eventDrawButtonPosition,
|
|
eventHandGeometry,
|
|
eventCardPosition
|
|
} from '../../src/game/objects/eventHandLayout'
|
|
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).
|
|
const CANVAS_W = 1920
|
|
const CANVAS_H = 1080
|
|
|
|
describe('game/objects/eventHandLayout', () => {
|
|
describe('eventDrawButtonPosition', () => {
|
|
it('positionne le bouton dans le quart bas-droit du canvas', () => {
|
|
const { x, y } = eventDrawButtonPosition(CANVAS_W, CANVAS_H)
|
|
expect(x).toBeGreaterThan(CANVAS_W / 2)
|
|
expect(y).toBeGreaterThan(CANVAS_H / 2)
|
|
})
|
|
|
|
it('reste entièrement à l\'intérieur du canvas (ne déborde jamais à droite ni en bas)', () => {
|
|
const buttonWidth = 190
|
|
const buttonHeight = 30
|
|
const { x, y } = eventDrawButtonPosition(CANVAS_W, CANVAS_H, buttonWidth)
|
|
expect(x + buttonWidth / 2).toBeLessThanOrEqual(CANVAS_W)
|
|
expect(x - buttonWidth / 2).toBeGreaterThanOrEqual(0)
|
|
expect(y + buttonHeight / 2).toBeLessThanOrEqual(CANVAS_H)
|
|
})
|
|
|
|
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 buttonBottom = btnY + buttonHeight / 2
|
|
const handTop = handY - ICON_SIZE / 2
|
|
expect(buttonBottom).toBeLessThanOrEqual(handTop)
|
|
})
|
|
|
|
it('reste bien à droite du bouton "CONFIRMER MA CARTE" (centré, ~280px de large) — pas de chevauchement', () => {
|
|
const confirmButtonWidth = 280
|
|
const confirmButtonRightEdge = CANVAS_W / 2 + confirmButtonWidth / 2
|
|
const drawButtonWidth = 190
|
|
const { x } = eventDrawButtonPosition(CANVAS_W, CANVAS_H, drawButtonWidth)
|
|
const drawButtonLeftEdge = x - drawButtonWidth / 2
|
|
// Régression du bug signalé : l'ancien placement (x = width/2 + 210,
|
|
// même Y que le bouton confirmer) chevauchait physiquement son bord
|
|
// droit de ~25px. Le nouveau placement bas-droite doit être
|
|
// verticalement séparé (voir test précédent) ET ne pas non plus se
|
|
// chevaucher horizontalement s'ils finissaient à la même hauteur.
|
|
expect(drawButtonLeftEdge).toBeGreaterThanOrEqual(confirmButtonRightEdge)
|
|
})
|
|
|
|
it('accepte une largeur de bouton personnalisée', () => {
|
|
const narrow = eventDrawButtonPosition(CANVAS_W, CANVAS_H, 100)
|
|
const wide = eventDrawButtonPosition(CANVAS_W, CANVAS_H, 300)
|
|
// Un bouton plus large doit être centré plus loin du bord droit
|
|
expect(narrow.x).toBeGreaterThan(wide.x)
|
|
})
|
|
})
|
|
|
|
describe('eventHandGeometry', () => {
|
|
it('positionne la main dans la moitié basse du canvas', () => {
|
|
const { y } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
|
|
expect(y).toBeGreaterThan(CANVAS_H / 2)
|
|
})
|
|
|
|
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 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, 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)
|
|
})
|
|
|
|
it('gère cardCount=0 sans produire de position invalide (NaN/Infinity)', () => {
|
|
const { startX, y, spacing } = eventHandGeometry(CANVAS_W, CANVAS_H, 0)
|
|
expect(Number.isFinite(startX)).toBe(true)
|
|
expect(Number.isFinite(y)).toBe(true)
|
|
expect(Number.isFinite(spacing)).toBe(true)
|
|
})
|
|
|
|
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 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 } = 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)
|
|
})
|
|
})
|
|
|
|
describe('eventCardPosition', () => {
|
|
it('retourne la même position que eventHandGeometry pour un index donné', () => {
|
|
const { startX, y, spacing } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
|
|
const pos0 = eventCardPosition(CANVAS_W, CANVAS_H, 2, 0)
|
|
const pos1 = eventCardPosition(CANVAS_W, CANVAS_H, 2, 1)
|
|
expect(pos0).toEqual({ x: startX, y })
|
|
expect(pos1).toEqual({ x: startX + spacing, y })
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|
|
|
|
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 } = 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(firstLeftEdge).toBeGreaterThan(CANVAS_W / 2)
|
|
})
|
|
})
|
|
})
|