/** * Fonctions d'aide partagées par les tests E2E Regnum. * * Toutes les interactions avec le canvas Phaser passent par un hook de debug * injecté via `page.addInitScript` (pas de modification du code source de * l'application) qui expose chaque instance `Phaser.Game` créée sur * `window.__ALL_GAMES__`. Cela permet d'introspecter l'état réel de la scène * (position des cartes/boutons, contenu de la main, etc.) plutôt que de * deviner des coordonnées fixes à l'écran — ce qui serait fragile face aux * changements de mise en page (voir historique des bugs de resize corrigés). */ /** Script à injecter via page.addInitScript avant toute navigation. */ export function phaserDebugHookInitScript() { window.__ALL_GAMES__ = [] const patchWhenReady = () => { if (window.Phaser && window.Phaser.Game && !window.Phaser.Game.__patched__) { const OrigGame = window.Phaser.Game function PatchedGame(config) { const inst = new OrigGame(config) window.__ALL_GAMES__.push(inst) return inst } PatchedGame.prototype = OrigGame.prototype PatchedGame.__patched__ = true window.Phaser.Game = PatchedGame } else { setTimeout(patchWhenReady, 5) } } patchWhenReady() } /** Installe le hook de debug Phaser sur une page (à appeler avant page.goto). */ export async function installPhaserDebugHook(page) { await page.addInitScript(phaserDebugHookInitScript) } /** Récupère la scène GameScene active dans le dernier jeu Phaser créé sur la page. */ export async function getGameScene(page) { return page.evaluateHandle(() => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] return game.scene.getScene('GameScene') }) } /** Crée une salle (P1) et retourne son roomId. */ export async function createRoom(page, playerName) { await page.goto('/') await page.fill('input[placeholder="Ton nom de joueur"]', playerName) await page.click('button:has-text("Créer une partie")') await page.waitForURL('**/lobby/**') return page.url().split('/lobby/')[1] } /** Rejoint une salle existante. */ export async function joinRoom(page, playerName, roomId) { await page.goto('/') await page.fill('input[placeholder="Ton nom de joueur"]', playerName) await page.fill('input[placeholder="Code de la salle"]', roomId) await page.click('button:has-text("Rejoindre")') await page.waitForURL('**/lobby/**') } /** Démarre la partie depuis le lobby (doit être appelé par l'hôte, P1). */ export async function startGame(page) { await page.click('button:has-text("Lancer la partie")') await page.waitForURL('**/game/**') } /** * Joue une carte de la main du joueur et confirme. Sélectionne dynamiquement * la carte MAXIMISANT (mode='max', défaut) ou MINIMISANT (mode='min') la * SOMME des attributs donnés — `attr` accepte une chaîne ('war') ou un * tableau (['health', 'diplo']). Sans argument, joue la première carte de la * main. Lit l'état réel de la scène — jamais de coordonnées fixes, pour * rester robuste face aux changements de layout. Ne fait rien si la main est * vide (fin de partie). * * Le mode 'min' est indispensable pour fiabiliser les tests d'accumulation * de jauge : le gain d'un tour = max(0, ma_valeur - valeur_du_voisin_de_gauche), * donc pour garantir un gain non-nul sur un attribut donné, il ne suffit pas * que l'adversaire joue "un autre attribut" (sa carte peut avoir une valeur * é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') { 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 scene = game?.scene?.getScene('GameScene') return scene?.handCards?.length ?? 0 }) if (handLen === 0) return false const attrs = attr === null ? null : (Array.isArray(attr) ? attr : [attr]) const cardPos = await page.evaluate(({ attrs, mode }) => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') let idx = 0 if (attrs) { let bestVal = mode === 'min' ? Infinity : -1 scene.handCards.forEach((c, i) => { const sum = attrs.reduce((s, a) => s + (c.cardData[a] ?? 0), 0) const better = mode === 'min' ? sum < bestVal : sum > bestVal if (better) { bestVal = sum; idx = i } }) } const b = scene.handCards[idx].getBounds() return { x: b.centerX, y: b.centerY } }, { attrs, mode }) await page.mouse.click(box.x + cardPos.x, box.y + cardPos.y) await page.waitForTimeout(200) const btnPos = await page.evaluate(() => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') const b = scene.playBtn.getBounds() return { x: b.centerX, y: b.centerY } }) await page.mouse.click(box.x + btnPos.x, box.y + btnPos.y) await page.waitForTimeout(200) return true } /** * 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 * 3.5s dans index.js + marge de sécurité). * * attrsByPage[i] peut être : null (première carte), une chaîne/tableau * d'attributs (mode 'max' implicite), ou { attr, mode } pour choisir * explicitement 'max' ou 'min' (voir playCard). */ export async function playFullTurn(pages, attrsByPage = []) { for (let i = 0; i < pages.length; i++) { const spec = attrsByPage[i] ?? null if (spec && typeof spec === 'object' && !Array.isArray(spec)) { await playCard(pages[i], spec.attr, spec.mode ?? 'max') } else { await playCard(pages[i], spec) } } await pages[0].waitForTimeout(4200) } /** * Survole un jeton d'objet de la boutique (colonne à gauche du plateau) par * son id, et retourne le centre du bouton ACHETER si l'objet est disponible * et achetable. Retourne null si le jeton n'existe pas (déjà acheté) ou si le * bouton n'est pas interactif (jauge insuffisante). */ export async function hoverShopItem(page, itemId) { const canvas = page.locator('#phaser-container canvas') const box = await canvas.boundingBox() const tokenPos = await page.evaluate((itemId) => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') const token = scene.itemTokens.find(t => t.itemData.id === itemId) return token ? { x: token.x, y: token.y } : null }, itemId) if (!tokenPos) return null await page.mouse.move(box.x + tokenPos.x, box.y + tokenPos.y) await page.waitForTimeout(300) return page.evaluate((itemId) => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') const token = scene.itemTokens.find(t => t.itemData.id === itemId) if (!token || !token._expanded) return null const btnBg = token._expanded.list.find(o => o.type === 'Graphics' && o.input) if (!btnBg) return null // bouton non interactif (jauge insuffisante) const worldPoint = btnBg.getWorldTransformMatrix().transformPoint( btnBg.input.hitArea.centerX, btnBg.input.hitArea.centerY ) return { x: worldPoint.x, y: worldPoint.y } }, itemId) } /** Clique sur le bouton ACHETER d'un objet actuellement affiché en survol. */ export async function buyShopItem(page, itemId) { const btnCenter = await hoverShopItem(page, itemId) if (!btnCenter) return false const canvas = page.locator('#phaser-container canvas') const box = await canvas.boundingBox() await page.mouse.click(box.x + btnCenter.x, box.y + btnCenter.y) await page.waitForTimeout(400) return true } /** Lit les jauges du joueur local depuis le store Pinia exposé par la scène. */ export async function getMyGauges(page) { return page.evaluate(() => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') const me = scene.store.players.find(p => p.teamIndex === scene.store.myTeamIndex) return me?.gauges ?? { food: 0, war: 0, health: 0, diplo: 0 } }) } /** Lit l'état de la boutique (ids d'objets encore affichés dans la colonne). */ export async function getShopItemIds(page) { return page.evaluate(() => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') return scene.itemTokens.map(t => t.itemData.id) }) } /** * Survole un jeton de bâtiment de la boutique (colonne à droite du plateau) * par son id, et retourne le centre du bouton ACHETER si disponible et * achetable. Retourne null si le jeton n'existe pas (déjà acheté) ou si le * bouton n'est pas interactif (jauge insuffisante sur au moins un attribut). */ export async function hoverShopBuilding(page, buildingId) { const canvas = page.locator('#phaser-container canvas') const box = await canvas.boundingBox() const tokenPos = await page.evaluate((buildingId) => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') const token = scene.buildingTokens.find(t => t.itemData.id === buildingId) return token ? { x: token.x, y: token.y } : null }, buildingId) if (!tokenPos) return null await page.mouse.move(box.x + tokenPos.x, box.y + tokenPos.y) await page.waitForTimeout(300) return page.evaluate((buildingId) => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') const token = scene.buildingTokens.find(t => t.itemData.id === buildingId) if (!token || !token._expanded) return null const btnBg = token._expanded.list.find(o => o.type === 'Graphics' && o.input) if (!btnBg) return null // bouton non interactif (jauge insuffisante) const worldPoint = btnBg.getWorldTransformMatrix().transformPoint( btnBg.input.hitArea.centerX, btnBg.input.hitArea.centerY ) return { x: worldPoint.x, y: worldPoint.y } }, buildingId) } /** Clique sur le bouton ACHETER d'un bâtiment actuellement affiché en survol. */ export async function buyShopBuilding(page, buildingId) { const btnCenter = await hoverShopBuilding(page, buildingId) if (!btnCenter) return false const canvas = page.locator('#phaser-container canvas') const box = await canvas.boundingBox() await page.mouse.click(box.x + btnCenter.x, box.y + btnCenter.y) await page.waitForTimeout(400) return true } /** Lit l'état de la boutique de bâtiments (ids encore affichés dans la colonne à droite). */ export async function getShopBuildingIds(page) { return page.evaluate(() => { const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1] const scene = game.scene.getScene('GameScene') return scene.buildingTokens.map(t => t.itemData.id) }) }