diff --git a/client/src/game/scenes/GameScene.js b/client/src/game/scenes/GameScene.js index 2282e77..9a26263 100644 --- a/client/src/game/scenes/GameScene.js +++ b/client/src/game/scenes/GameScene.js @@ -1186,7 +1186,20 @@ export class GameScene extends Phaser.Scene { this._lastRoundResult = null this.phaseText.setText('Choisissez une carte puis confirmez') this.readyText.setText('') - this._deactivateBtn() + // Ne réinitialiser visuellement le bouton QUE si aucune carte n'est + // déjà sélectionnée. Ce listener 'game-state' se déclenche à chaque + // action de N'IMPORTE QUEL joueur (ex: un adversaire qui pioche un + // événement ou achète un objet), pas seulement au vrai changement de + // tour — sans cette garde, _deactivateBtn() désactivait visuellement + // le bouton "CONFIRMER MA CARTE" alors que this.selectedCard restait + // valide en mémoire (toujours sélectionnée, toujours surélevée + // visuellement), désynchronisant le bouton de l'état réel de la + // carte : un premier clic sur la carte la désélectionnait (elle + // était déjà _selected=true côté CardObject), il fallait alors + // recliquer une seconde fois pour la vraiment resélectionner. + if (!this.selectedCard) { + this._deactivateBtn() + } // (Re)construire les slots si le nombre de joueurs a changé (premier // tour), OU si la liste d'objets/bâtiments possédés par un adversaire // a changé (leurs mini-cartes, affichées sous leur slot, doivent diff --git a/e2e/tests/card-selection-persistence.spec.js b/e2e/tests/card-selection-persistence.spec.js new file mode 100644 index 0000000..c719a7a --- /dev/null +++ b/e2e/tests/card-selection-persistence.spec.js @@ -0,0 +1,104 @@ +import { test, expect } from '@playwright/test' +import { + installPhaserDebugHook, createRoom, joinRoom, startGame, + selectHandCard, confirmSelectedCard, playFullTurn, getMyGauges, clickEventDrawButton +} from './helpers.js' + +/** + * Régression : sélectionner une carte de la main, puis effectuer une autre + * action réseau AVANT de confirmer (ex: piocher un événement) ne devait pas + * désynchroniser le bouton "CONFIRMER MA CARTE" de l'état réel de sélection. + * + * Cause du bug : le listener 'game-state' (déclenché à CHAQUE action de + * N'IMPORTE QUEL joueur, pas seulement au changement de tour) appelait + * inconditionnellement _deactivateBtn() dès que phase==='selecting', même si + * this.selectedCard restait valide en mémoire. La carte restait visuellement + * sélectionnée (surélevée, CardObject._selected=true) mais le bouton de + * confirmation apparaissait grisé — un premier clic sur la carte la + * DÉSÉLECTIONNAIT (déjà _selected=true côté CardObject) au lieu de la + * confirmer, obligeant à cliquer une seconde fois pour la re-sélectionner. + */ +test.describe('Persistance de la sélection de carte personnage', () => { + test('sélectionner une carte puis piocher un événement ne désactive pas le bouton de confirmation', async ({ browser }) => { + test.setTimeout(90_000) + + const ctx1 = await browser.newContext() + const ctx2 = await browser.newContext() + const p1 = await ctx1.newPage() + const p2 = await ctx2.newPage() + const errors = [] + p1.on('pageerror', e => errors.push(e.message)) + await installPhaserDebugHook(p1) + await installPhaserDebugHook(p2) + + const roomId = await createRoom(p1, 'Alice') + await joinRoom(p2, 'Bob', roomId) + await startGame(p1) + await p1.waitForTimeout(2000) + + // Accumuler assez de points sur les 4 jauges pour pouvoir piocher un événement. + let gauges = await getMyGauges(p1) + let attempts = 0 + while (!['food', 'war', 'health', 'diplo'].every(a => gauges[a] >= 1) && attempts < 12) { + await playFullTurn([p1, p2], [ + ['food', 'war', 'health', 'diplo'], + { attr: ['food', 'war', 'health', 'diplo'], mode: 'min' } + ]) + gauges = await getMyGauges(p1) + attempts++ + } + expect(['food', 'war', 'health', 'diplo'].every(a => gauges[a] >= 1)).toBe(true) + + // Sélectionner une carte personnage SANS la confirmer. + const selected = await selectHandCard(p1) + expect(selected).toBe(true) + + const selectedCardIdBefore = await p1.evaluate(() => { + const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] + const scene = game.scene.getScene('GameScene') + return scene.selectedCard?.cardData?.id ?? null + }) + expect(selectedCardIdBefore).not.toBeNull() + + // Déclencher une action réseau qui provoque un game-state broadcast à + // tous les joueurs (piocher un événement), SANS toucher à la carte. + const drew = await clickEventDrawButton(p1) + expect(drew).toBe(true) + await p1.waitForTimeout(300) + + // La carte doit rester sélectionnée EN MÉMOIRE... + const selectedCardIdAfter = await p1.evaluate(() => { + const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] + const scene = game.scene.getScene('GameScene') + return scene.selectedCard?.cardData?.id ?? null + }) + expect(selectedCardIdAfter).toBe(selectedCardIdBefore) + + // ...ET le bouton de confirmation doit rester visuellement ACTIF (pas de + // pulse-tween arrêté par un _deactivateBtn() intempestif). + const btnActive = await p1.evaluate(() => { + const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] + const scene = game.scene.getScene('GameScene') + return !!scene._pulseTween && scene._pulseTween.isPlaying() + }) + expect(btnActive).toBe(true) + + // Confirmer directement (SANS recliquer sur la carte au préalable) doit + // fonctionner du premier coup. + const confirmed = await confirmSelectedCard(p1) + expect(confirmed).toBe(true) + await p1.waitForTimeout(300) + + const phaseAfterConfirm = await p1.evaluate(() => { + const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] + const scene = game.scene.getScene('GameScene') + return scene.phaseText.text + }) + expect(phaseAfterConfirm).toMatch(/attente/) + + expect(errors).toEqual([]) + + await ctx1.close() + await ctx2.close() + }) +}) diff --git a/e2e/tests/helpers.js b/e2e/tests/helpers.js index 04d4411..c7f1100 100644 --- a/e2e/tests/helpers.js +++ b/e2e/tests/helpers.js @@ -84,12 +84,20 @@ export async function startGame(page) { * élevée sur l'attribut visé par pur hasard) — il faut explicitement qu'il * joue sa carte la plus FAIBLE sur cet attribut précis. */ -export async function playCard(page, attr = null, mode = 'max') { +/** + * Clique sur une carte de la main (sans confirmer) pour la sélectionner. + * Séparé de playCard() pour pouvoir tester des scénarios où d'autres + * actions ont lieu ENTRE la sélection et la confirmation (ex: piocher un + * événement pendant qu'une carte personnage est déjà sélectionnée — voir + * la régression "il faut recliquer sur la carte pour la re-sélectionner"). + * Retourne false si la main est vide. + */ +export async function selectHandCard(page, attr = null, mode = 'max') { const canvas = page.locator('#phaser-container canvas') const box = await canvas.boundingBox() const handLen = await page.evaluate(() => { - const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] + const game = window.__ALL_GAMES__?.[window.__ALL_GAMES__.length - 1] const scene = game?.scene?.getScene('GameScene') return scene?.handCards?.length ?? 0 }) @@ -117,6 +125,13 @@ export async function playCard(page, attr = null, mode = 'max') { await page.mouse.click(box.x + cardPos.x, box.y + cardPos.y) await page.waitForTimeout(200) + return true +} + +/** Clique sur le bouton "CONFIRMER MA CARTE" (suppose une carte déjà sélectionnée). */ +export async function confirmSelectedCard(page) { + const canvas = page.locator('#phaser-container canvas') + const box = await canvas.boundingBox() const btnPos = await page.evaluate(() => { const game = window.__ALL_GAMES__?.[window.__ALL_GAMES__.length - 1] @@ -130,6 +145,13 @@ export async function playCard(page, attr = null, mode = 'max') { return true } +/** Sélectionne une carte de la main puis la confirme immédiatement (comportement historique de playCard). */ +export async function playCard(page, attr = null, mode = 'max') { + const selected = await selectHandCard(page, attr, mode) + if (!selected) return false + return confirmSelectedCard(page) +} + /** * Joue un tour complet pour une liste de pages (dans l'ordre donné), puis * attend la résolution + le début du tour suivant (délai serveur fixe de