fix: repositionner les cartes Événement en bas-droite avec un texte lisible
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).
This commit is contained in:
1 parent
c9b4a7e567
commit
45eb9be60d
4 files changed
+263
-26
No files matched your search
@@ -55,18 +55,29 @@ export class EventCardObject extends Phaser.GameObjects.Container {
|
||||
border.strokeRoundedRect(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H, RADIUS)
|
||||
this.add(border)
|
||||
|
||||
// ── 4. Nom en bas ──────────────────────────────────────────────────────────
|
||||
// ── 4. Bandeau + nom en bas ──────────────────────────────────────────────
|
||||
// Un fond semi-opaque derrière le texte (au lieu du seul contour) garde
|
||||
// le nom lisible quel que soit le portrait en arrière-plan, cohérent
|
||||
// avec l'affichage renforcé demandé pour cette carte (échelle réduite
|
||||
// en main : voir EVENT_CARD_SCALE dans eventHandLayout.js).
|
||||
const nameBandH = 34
|
||||
const nameBand = this.scene.add.graphics()
|
||||
nameBand.fillStyle(0x000000, 0.55)
|
||||
nameBand.fillRect(-CARD_W / 2 + 3, CARD_H / 2 - nameBandH, CARD_W - 6, nameBandH - 3)
|
||||
this.add(nameBand)
|
||||
|
||||
this.add(this.scene.add.text(
|
||||
0, CARD_H / 2 - 14,
|
||||
0, CARD_H / 2 - nameBandH / 2 - 1,
|
||||
this.eventData.name, {
|
||||
fontSize: '12px',
|
||||
fontSize: '15px',
|
||||
fontFamily: 'Georgia, serif',
|
||||
fontStyle: 'bold',
|
||||
color: '#f5e6c8',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 3,
|
||||
align: 'center',
|
||||
wordWrap: { width: CARD_W - 12 }
|
||||
lineSpacing: 2,
|
||||
wordWrap: { width: CARD_W - 10 }
|
||||
}
|
||||
).setOrigin(0.5, 0.5))
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Calculs purs de géométrie pour la zone "cartes Événement" (bouton de
|
||||
* pioche + main de cartes événement, max 2), extraits de GameScene pour
|
||||
* être testables unitairement sans dépendre de Phaser/Canvas.
|
||||
*
|
||||
* Placement choisi : bas-DROITE de l'écran, clairement séparé du bouton
|
||||
* "CONFIRMER MA CARTE" (centré en bas) et de la main de personnages
|
||||
* (centrée en bas) pour éviter toute confusion entre "jouer une carte
|
||||
* personnage" et "jouer une carte événement" — un chevauchement précédent
|
||||
* plaçait le bouton de pioche à ~25px du bord du bouton de confirmation.
|
||||
*/
|
||||
|
||||
import { CARD_W, CARD_H } from './cardLayout'
|
||||
|
||||
// Échelle appliquée aux cartes événement en main. Volontairement proche de 1
|
||||
// (0.8, pas 0.55 comme l'ancien placement) pour que le nom de l'événement
|
||||
// (police 12px de base) reste réellement lisible une fois mis à l'échelle.
|
||||
export const EVENT_CARD_SCALE = 0.8
|
||||
|
||||
// Marge depuis les bords droit/bas du canvas.
|
||||
const MARGIN_RIGHT = 20
|
||||
const MARGIN_BOTTOM = 60 // au-dessus de BOTTOM_MARGIN (barre Windows) + marge de confort
|
||||
|
||||
// Hauteur réservée au bouton de pioche + son espacement avec la main.
|
||||
const DRAW_BUTTON_H = 30
|
||||
const GAP_BUTTON_HAND = 14
|
||||
|
||||
/**
|
||||
* Position du bouton "Piocher un Événement", ancré en bas à droite du
|
||||
* canvas, au-dessus de la main de cartes événement elle-même.
|
||||
*/
|
||||
export function eventDrawButtonPosition(canvasWidth, canvasHeight, buttonWidth = 190) {
|
||||
const x = canvasWidth - MARGIN_RIGHT - buttonWidth / 2
|
||||
const y = canvasHeight - MARGIN_BOTTOM - (CARD_H * EVENT_CARD_SCALE) - GAP_BUTTON_HAND - DRAW_BUTTON_H / 2
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
/**
|
||||
* Géométrie de la main de cartes événement (jusqu'à 2), positionnée juste
|
||||
* sous le bouton de pioche, alignée sur le bord droit du canvas (les cartes
|
||||
* s'empilent horizontalement vers la GAUCHE depuis le bord droit).
|
||||
*/
|
||||
export function eventHandGeometry(canvasWidth, canvasHeight, cardCount) {
|
||||
const cardW = CARD_W * EVENT_CARD_SCALE
|
||||
const cardH = CARD_H * EVENT_CARD_SCALE
|
||||
const spacing = cardW + 10
|
||||
|
||||
const y = canvasHeight - MARGIN_BOTTOM - cardH / 2
|
||||
// Ancrées sur le bord droit : la dernière carte (index cardCount-1) a son
|
||||
// bord droit à `canvasWidth - MARGIN_RIGHT`, les précédentes s'étendent
|
||||
// vers la gauche.
|
||||
const rightEdge = canvasWidth - MARGIN_RIGHT
|
||||
const lastCardCenterX = rightEdge - cardW / 2
|
||||
const startX = lastCardCenterX - Math.max(cardCount - 1, 0) * spacing
|
||||
|
||||
return { startX, y, spacing, scale: EVENT_CARD_SCALE }
|
||||
}
|
||||
|
||||
/** Position (x, y) de la carte d'index `i` dans la main d'événements. */
|
||||
export function eventCardPosition(canvasWidth, canvasHeight, cardCount, index) {
|
||||
const { startX, y, spacing } = eventHandGeometry(canvasWidth, canvasHeight, cardCount)
|
||||
return { x: startX + index * spacing, y }
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { CardObject } from '../objects/CardObject'
|
||||
import { CARD_W, CARD_H } from '../objects/cardLayout'
|
||||
import { ItemBoardToken, ICON_SIZE } from '../objects/ItemBoardToken'
|
||||
import { EventCardObject } from '../objects/EventCardObject'
|
||||
import { eventDrawButtonPosition, eventHandGeometry, EVENT_CARD_SCALE } from '../objects/eventHandLayout'
|
||||
import { ITEMS, getItemById } from '../items'
|
||||
import { BUILDINGS, getBuildingById } from '../buildings'
|
||||
import { EVENTS, getEventById, DRAW_COST } from '../events'
|
||||
@@ -156,9 +157,13 @@ export class GameScene extends Phaser.Scene {
|
||||
this.playSlots = []
|
||||
this.slotObjects = [] // pour pouvoir les détruire/recréer
|
||||
|
||||
// Légende attributs (coin en bas à droite, au-dessus de la marge Windows)
|
||||
// Légende attributs, coin en bas à GAUCHE (déplacée depuis la droite :
|
||||
// le coin bas-droit est désormais occupé par la zone "cartes Événement"
|
||||
// — bouton de pioche + main, voir eventHandLayout.js). Le bas-gauche
|
||||
// reste libre sous la colonne de la boutique d'objets (qui s'arrête
|
||||
// bien avant le bas de l'écran, voir _itemTokensStartY/_itemTokensSpacing).
|
||||
this.legendObjects = []
|
||||
this._buildAttrLegend(width - 10, height - BOTTOM_MARGIN - 10)
|
||||
this._buildAttrLegend(10, height - BOTTOM_MARGIN - 10)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,8 +191,8 @@ export class GameScene extends Phaser.Scene {
|
||||
if (this.turnText) this.turnText.setPosition(width / 2, 18)
|
||||
if (this.phaseText) this.phaseText.setPosition(width / 2, 40)
|
||||
|
||||
// Légende attributs
|
||||
this._buildAttrLegend(width - 10, height - BOTTOM_MARGIN - 10)
|
||||
// Légende attributs (bas-gauche, voir _buildTable)
|
||||
this._buildAttrLegend(10, height - BOTTOM_MARGIN - 10)
|
||||
|
||||
// Slots de jeu. Reconstruire les slots est destructif et invaliderait les
|
||||
// coordonnées déjà utilisées par les cartes révélées à l'écran (phase
|
||||
@@ -303,7 +308,7 @@ export class GameScene extends Phaser.Scene {
|
||||
fontSize: '10px',
|
||||
fontFamily: 'Georgia',
|
||||
color: '#6a5040'
|
||||
}).setOrigin(1, 1)
|
||||
}).setOrigin(0, 1) // ancré à GAUCHE de (x, y) — légende désormais en bas-gauche
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -649,10 +654,18 @@ export class GameScene extends Phaser.Scene {
|
||||
|
||||
// ─── Cartes Événement (pioche + seconde main + ciblage) ────────────────────
|
||||
|
||||
/** Position du bouton de pioche d'événement : à droite du bouton de confirmation, même Y. */
|
||||
/**
|
||||
* Position du bouton de pioche d'événement : bas-DROITE du canvas, au
|
||||
* dessus de la main de cartes événement. Calcul délégué à
|
||||
* eventHandLayout.js (module pur, testé unitairement) pour rester
|
||||
* clairement séparé du bouton "CONFIRMER MA CARTE" (centré en bas) — un
|
||||
* placement précédent (à droite du bouton de confirmation, même Y) se
|
||||
* chevauchait physiquement avec lui (280px de large, bords qui se
|
||||
* recouvraient de ~25px), prêtant à confusion entre les deux actions.
|
||||
*/
|
||||
_eventDrawButtonPos() {
|
||||
const { width } = this.scale
|
||||
return { x: width / 2 + 210, y: this._playButtonY() }
|
||||
const { width, height } = this.scale
|
||||
return eventDrawButtonPosition(width, height)
|
||||
}
|
||||
|
||||
_buildEventDrawButton() {
|
||||
@@ -718,16 +731,12 @@ export class GameScene extends Phaser.Scene {
|
||||
this.socket.emit('buy-event-draw', { roomId: this.roomId })
|
||||
}
|
||||
|
||||
/** Position/espacement de ma main de cartes événement (max 2), au-dessus de mon bouton de pioche. */
|
||||
_eventHandGeometry() {
|
||||
const { x } = this._eventDrawButtonPos()
|
||||
const y = this._playButtonY() - 100
|
||||
return { x, y, spacing: 90 }
|
||||
}
|
||||
|
||||
/**
|
||||
* (Re)construit ma main de cartes événement (max EVENT_HAND_MAX=2), à
|
||||
* l'échelle réduite pour rester compacte au-dessus du bouton de pioche.
|
||||
* (Re)construit ma main de cartes événement (max EVENT_HAND_MAX=2), en
|
||||
* bas-droite du canvas, à une échelle (0.8, voir eventHandLayout.js)
|
||||
* suffisamment proche de 1 pour que le nom de l'événement reste lisible
|
||||
* — un placement précédent utilisait 0.55, rendant le texte (12px de
|
||||
* base) quasi illisible une fois réduit.
|
||||
*/
|
||||
_buildEventHand(eventIds) {
|
||||
this.eventCards.forEach(c => c.destroy())
|
||||
@@ -736,14 +745,14 @@ export class GameScene extends Phaser.Scene {
|
||||
|
||||
if (!eventIds || eventIds.length === 0) return
|
||||
|
||||
const { x, y, spacing } = this._eventHandGeometry()
|
||||
const startX = x - (eventIds.length - 1) * spacing / 2
|
||||
const { width, height } = this.scale
|
||||
const { startX, y, spacing, scale } = eventHandGeometry(width, height, eventIds.length)
|
||||
|
||||
eventIds.forEach((eventId, i) => {
|
||||
const eventData = getEventById(eventId)
|
||||
if (!eventData) return
|
||||
const card = new EventCardObject(this, startX + i * spacing, y, eventData)
|
||||
card.setScale(0.55)
|
||||
card.setScale(scale)
|
||||
card.on('select', this._onEventCardSelected, this)
|
||||
this.eventCards.push(card)
|
||||
})
|
||||
@@ -751,8 +760,8 @@ export class GameScene extends Phaser.Scene {
|
||||
|
||||
_layoutEventHand() {
|
||||
if (this.eventCards.length === 0) return
|
||||
const { x, y, spacing } = this._eventHandGeometry()
|
||||
const startX = x - (this.eventCards.length - 1) * spacing / 2
|
||||
const { width, height } = this.scale
|
||||
const { startX, y, spacing } = eventHandGeometry(width, height, this.eventCards.length)
|
||||
this.eventCards.forEach((card, i) => {
|
||||
card.setPosition(startX + i * spacing, y)
|
||||
card._baseY = y
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
eventDrawButtonPosition,
|
||||
eventHandGeometry,
|
||||
eventCardPosition,
|
||||
EVENT_CARD_SCALE
|
||||
} from '../../src/game/objects/eventHandLayout'
|
||||
import { CARD_W, CARD_H } from '../../src/game/objects/cardLayout'
|
||||
|
||||
// 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('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)
|
||||
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 cartes é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
|
||||
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('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('avec 1 carte, elle est ancrée 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)
|
||||
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 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('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('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)
|
||||
})
|
||||
})
|
||||
|
||||
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 cartes 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, scale } = eventHandGeometry(CANVAS_W, CANVAS_H, 2)
|
||||
const cardW = CARD_W * scale
|
||||
const firstCardLeftEdge = startX - cardW / 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user