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).
450 lines
19 KiB
JavaScript
450 lines
19 KiB
JavaScript
/**
|
|
* 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')
|
|
if (!scene?.handCards?.length) return null
|
|
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 })
|
|
if (!cardPos) return false
|
|
|
|
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 b ? { x: b.centerX, y: b.centerY } : null
|
|
})
|
|
if (!btnPos) return false
|
|
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) ?? []
|
|
})
|
|
}
|
|
|
|
// ── Cartes Événement ─────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Lit ma main d'événements actuelle (ids, max 2) depuis le store Pinia
|
|
* exposé par la scène. Défensif face au timing : `GameScene.create()` (qui
|
|
* assigne `this.store`) peut ne pas encore avoir tourné juste après un
|
|
* changement de scène/tour, notamment sous charge avec plusieurs pages
|
|
* Playwright simultanées — plutôt que planter, on retourne un tableau vide
|
|
* (état "pas encore prêt", cohérent avec drawEventUntil qui retentera).
|
|
*/
|
|
export async function getMyEventHand(page) {
|
|
return page.evaluate(() => {
|
|
const game = window.__ALL_GAMES__?.[window.__ALL_GAMES__.length - 1]
|
|
const scene = game?.scene?.getScene('GameScene')
|
|
return scene?.store?.myEventHand ?? []
|
|
})
|
|
}
|
|
|
|
/** Clique sur le bouton de pioche d'événement (si actuellement cliquable). */
|
|
export async function clickEventDrawButton(page) {
|
|
const canvas = page.locator('#phaser-container canvas')
|
|
const box = await canvas.boundingBox()
|
|
const btnInfo = await page.evaluate(() => {
|
|
const game = window.__ALL_GAMES__?.[window.__ALL_GAMES__.length - 1]
|
|
const scene = game?.scene?.getScene('GameScene')
|
|
const btn = scene?.eventDrawBtn
|
|
if (!btn || !btn.input || !btn.input.enabled) return null
|
|
const b = btn.getBounds()
|
|
return { x: b.centerX, y: b.centerY }
|
|
})
|
|
if (!btnInfo) return false
|
|
await page.mouse.click(box.x + btnInfo.x, box.y + btnInfo.y)
|
|
await page.waitForTimeout(400)
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Pioche des cartes événement jusqu'à obtenir l'id demandé (ou jusqu'à
|
|
* atteindre maxDraws tentatives). Le deck partagé est mélangé aléatoirement
|
|
* (18 cartes, 3 exemplaires de chacun des 6 événements) donc aucune pioche
|
|
* spécifique n'est garantie à coup sûr — mais avec un nombre de tentatives
|
|
* suffisant sur un deck de cette taille, la probabilité de ne jamais tomber
|
|
* sur l'événement recherché reste négligeable en pratique de test.
|
|
*
|
|
* IMPORTANT : cette fonction gère l'espacement en tours nécessaire pour
|
|
* reconstituer les jauges entre deux pioches (coût de 1 point dans chacune
|
|
* des 4 jauges) — l'appelant doit fournir `otherPages` pour que playFullTurn
|
|
* puisse faire progresser les tours pendant l'accumulation.
|
|
*/
|
|
export async function drawEventUntil(page, otherPages, eventId, maxDraws = 20) {
|
|
// Événements sans ciblage requis (au pire un ciblage optionnel côté
|
|
// Épidémie, déclenché seulement avec la Trousse — jamais possédée dans un
|
|
// scénario de test frais). Utilisés pour vider un slot de la main sans
|
|
// jamais déclencher une séquence de ciblage qui resterait bloquée faute
|
|
// de clic sur un slot adverse (playEventCard sans targetTeamIndexes).
|
|
const SAFE_TO_DISCARD = new Set(['bountiful_harvest', 'troop_levy', 'plot_uncovered', 'castle_feast', 'epidemic'])
|
|
|
|
for (let attempt = 0; attempt < maxDraws; attempt++) {
|
|
const hand = await getMyEventHand(page)
|
|
if (hand.includes(eventId)) return true
|
|
|
|
if (hand.length >= 2) {
|
|
// Main pleine SANS l'événement recherché : au lieu d'abandonner (ce qui
|
|
// rendait ce helper très peu fiable — avec seulement 6 types
|
|
// d'événements et une main de 2, l'échec dès les 2 premières pioches
|
|
// est fréquent, pas rare), jouer l'une des 2 cartes en main pour
|
|
// libérer un slot et pouvoir continuer à piocher.
|
|
// IMPORTANT : ne JAMAIS choisir 'royal_accord' pour cette défausse —
|
|
// il exige un ciblage séquentiel à 2 étapes (gain + perte) ; le jouer
|
|
// sans fournir targetTeamIndexes laisserait la séquence de ciblage
|
|
// bloquée côté client (aucun 'play-event' jamais émis), et donc la
|
|
// main jamais libérée, causant l'échec systématique observé.
|
|
const discardable = hand.find(id => SAFE_TO_DISCARD.has(id)) ?? hand[0]
|
|
if (discardable === 'royal_accord') {
|
|
// Les 2 cartes en main sont 'royal_accord' (cas rare) : impossible
|
|
// de la défausser sans ciblage séquentiel complet. On cible le
|
|
// premier adversaire réel (n'importe quel autre teamIndex que le
|
|
// mien) pour les deux étapes — le ciblage doit obligatoirement
|
|
// pointer sur un adversaire pour que le clic sur le slot déclenche
|
|
// réellement _onTargetSlotClicked (les highlights ne sont posés que
|
|
// sur les slots adverses, jamais sur le mien).
|
|
const myTeamIndex = await page.evaluate(() => {
|
|
const game = window.__ALL_GAMES__?.[window.__ALL_GAMES__.length - 1]
|
|
return game?.scene?.getScene('GameScene')?.store?.myTeamIndex ?? 0
|
|
})
|
|
const opponentTeamIndex = myTeamIndex === 0 ? 1 : 0
|
|
await playEventCard(page, discardable, [opponentTeamIndex, opponentTeamIndex])
|
|
} else {
|
|
await playEventCard(page, discardable)
|
|
}
|
|
await playFullTurn([page, ...otherPages])
|
|
continue
|
|
}
|
|
|
|
const gauges = await getMyGauges(page)
|
|
const affordable = ['food', 'war', 'health', 'diplo'].every(a => (gauges[a] ?? 0) >= 1)
|
|
|
|
if (affordable) {
|
|
const drew = await clickEventDrawButton(page)
|
|
if (drew) continue // retente immédiatement la vérification de main sans jouer de tour
|
|
}
|
|
|
|
// Jauges insuffisantes : jouer un tour complet (toutes les cartes au
|
|
// hasard, peu importe l'attribut) pour accumuler au moins 1 point sur
|
|
// chaque jauge avant de retenter.
|
|
await playFullTurn([page, ...otherPages])
|
|
}
|
|
return (await getMyEventHand(page)).includes(eventId)
|
|
}
|
|
|
|
/**
|
|
* Joue une carte de ma main d'événements ce tour. Les cartes événement
|
|
* utilisent désormais le même composant que les objets/bâtiments
|
|
* (ItemBoardToken, kind='event') : un jeton compact toujours visible, dont
|
|
* il faut survoler pour révéler la carte détaillée, puis cliquer sur son
|
|
* bouton "JOUER" — plus un simple clic direct sur une grande carte comme
|
|
* l'ancien EventCardObject.
|
|
*
|
|
* Si l'événement nécessite un ciblage, fournir `targetTeamIndexes`
|
|
* (tableau, dans l'ordre attendu par GameScene._startTargeting : pour
|
|
* royal_accord [gainTargetTeamIndex, loseTargetTeamIndex], pour epidemic
|
|
* [targetTeamIndex]) — les clics correspondants sur les slots adversaires
|
|
* sont effectués automatiquement après le clic sur "JOUER".
|
|
*/
|
|
export async function playEventCard(page, eventId, targetTeamIndexes = []) {
|
|
const canvas = page.locator('#phaser-container canvas')
|
|
const box = await canvas.boundingBox()
|
|
|
|
const tokenPos = await page.evaluate((eventId) => {
|
|
const game = window.__ALL_GAMES__?.[window.__ALL_GAMES__.length - 1]
|
|
const scene = game?.scene?.getScene('GameScene')
|
|
const token = scene?.eventCards?.find(c => c.itemData.id === eventId)
|
|
return token ? { x: token.x, y: token.y } : null
|
|
}, eventId)
|
|
if (!tokenPos) return false
|
|
|
|
await page.mouse.move(box.x + tokenPos.x, box.y + tokenPos.y)
|
|
await page.waitForTimeout(300)
|
|
|
|
const btnCenter = await page.evaluate((eventId) => {
|
|
const game = window.__ALL_GAMES__?.[window.__ALL_GAMES__.length - 1]
|
|
const scene = game?.scene?.getScene('GameScene')
|
|
const token = scene?.eventCards?.find(c => c.itemData.id === eventId)
|
|
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 (une autre carte déjà jouée ce tour)
|
|
const worldPoint = btnBg.getWorldTransformMatrix().transformPoint(
|
|
btnBg.input.hitArea.centerX, btnBg.input.hitArea.centerY
|
|
)
|
|
return { x: worldPoint.x, y: worldPoint.y }
|
|
}, eventId)
|
|
if (!btnCenter) return false
|
|
|
|
await page.mouse.click(box.x + btnCenter.x, box.y + btnCenter.y)
|
|
await page.waitForTimeout(250)
|
|
|
|
for (const teamIndex of targetTeamIndexes) {
|
|
const slotPos = await page.evaluate((teamIndex) => {
|
|
const game = window.__ALL_GAMES__?.[window.__ALL_GAMES__.length - 1]
|
|
const scene = game?.scene?.getScene('GameScene')
|
|
const idx = scene?.store?.players?.findIndex(p => p.teamIndex === teamIndex) ?? -1
|
|
if (idx === -1) return null
|
|
const slot = scene.playSlots[idx]
|
|
return slot ? { x: slot.x, y: slot.y } : null
|
|
}, teamIndex)
|
|
if (!slotPos) continue
|
|
await page.mouse.click(box.x + slotPos.x, box.y + slotPos.y)
|
|
await page.waitForTimeout(250)
|
|
}
|
|
|
|
return true
|
|
}
|