feat: bâtiments achetables (Église, Château Royal) avec synergie objets
- Nouveau server/src/game/buildings.js : 2 bâtiments, chacun couvrant 2 attributs, coût réparti sur les deux jauges, garantit +1 par tour sur chaque attribut, avec bonus de synergie +1 si l'objet correspondant est aussi possédé (Église+Trousse/Sceau, Château+Charrue/Trébuchet). - Room.js : buyBuilding() (validation coût multi-jauges), résolution des achats en attente avec la même priorité/stock unique que les objets, _applyBuildingEffects() (garantie de base + synergie, jamais de recul par rapport aux gains déjà boostés par un objet). getStateFor/getPlayersInfo exposent buildings/myBuildings/availableBuildings/pendingBuyBuildingId. - Nouvel event socket 'buy-building', boughtBuildings inclus dans round-result. - Client : buildings.js (miroir), BootScene génère les textures portrait des bâtiments (méthode _generatePortraitTexture généralisée, réutilisée avec les objets). ItemBoardToken généralisé (kind='item'|'building', hAnchor 'left'|'right') pour afficher indifféremment un objet ou un bâtiment, avec coût détaillé par jauge affiché sur la carte agrandie pour les bâtiments. - GameScene : boutique de bâtiments en colonne à DROITE du plateau (symétrique à la boutique d'objets à gauche), mes bâtiments possédés affichés à GAUCHE de ma main (symétrique à mes objets à droite), bâtiments des adversaires affichés sous leurs objets au niveau de leur slot. - Fix : la carte détaillée héritait par défaut d'un ancrage "s'étend vers la droite" adapté à la boutique d'objets (bord gauche du canvas) mais cassait la boutique de bâtiments (bord droit) — le bouton ACHETER se retrouvait hors du canvas et donc injoignable au clic. Ajout du paramètre hAnchor. - 26 nouveaux tests unitaires serveur (buildings.js + Room.js : validations d'achat multi-jauges, priorité/stock unique, effets de base et synergie). - 6 nouveaux tests E2E Playwright (boutique bâtiments : affichage, position, carte détaillée dans les limites du canvas, coût affiché, achat complet). - Fix flakiness des tests E2E d'achat (item-shop + building-shop) : le mode 'min' de playCard garantit désormais que l'adversaire joue sa carte la plus faible sur l'attribut ciblé, au lieu de simplement jouer un autre attribut (insuffisant, le gain dépend de la comparaison au voisin gauche).
This commit is contained in:
1 parent
11cba0467b
commit
9139989fa7
14 files changed
+1222
-89
No files matched your search
@@ -0,0 +1,248 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import {
|
||||
installPhaserDebugHook, createRoom, joinRoom, startGame,
|
||||
playFullTurn, getShopBuildingIds, getMyGauges, buyShopBuilding, hoverShopBuilding
|
||||
} from './helpers.js'
|
||||
|
||||
/**
|
||||
* Tests d'intégration E2E : boutique de bâtiments (Église, Château Royal).
|
||||
*
|
||||
* Couvre notamment la régression détectée manuellement : la carte détaillée
|
||||
* du bâtiment (affichée au survol) s'étendait par défaut vers la DROITE
|
||||
* depuis le jeton, comme pour la boutique d'objets à gauche du plateau —
|
||||
* mais la boutique de bâtiments est sur le bord DROIT du canvas, donc la
|
||||
* carte débordait hors du canvas et son bouton ACHETER devenait injoignable
|
||||
* au clic (coordonnées calculées hors des limites du canvas). Corrigé via le
|
||||
* paramètre `hAnchor='right'` sur ItemBoardToken pour cette boutique.
|
||||
*/
|
||||
test.describe('Boutique de bâtiments', () => {
|
||||
test('la boutique affiche les 2 bâtiments (Église, Château) au démarrage de la partie', async ({ browser }) => {
|
||||
const ctx1 = await browser.newContext()
|
||||
const ctx2 = await browser.newContext()
|
||||
const p1 = await ctx1.newPage()
|
||||
const p2 = await ctx2.newPage()
|
||||
await installPhaserDebugHook(p1)
|
||||
await installPhaserDebugHook(p2)
|
||||
|
||||
const roomId = await createRoom(p1, 'Alice')
|
||||
await joinRoom(p2, 'Bob', roomId)
|
||||
await startGame(p1)
|
||||
await p1.waitForTimeout(2000)
|
||||
|
||||
const ids = await getShopBuildingIds(p1)
|
||||
expect(ids.sort()).toEqual(['castle', 'church'].sort())
|
||||
|
||||
await ctx1.close()
|
||||
await ctx2.close()
|
||||
})
|
||||
|
||||
test('les jetons de bâtiment sont positionnés sur le bord DROIT du canvas', async ({ browser }) => {
|
||||
const ctx1 = await browser.newContext()
|
||||
const ctx2 = await browser.newContext()
|
||||
const p1 = await ctx1.newPage()
|
||||
const p2 = await ctx2.newPage()
|
||||
await installPhaserDebugHook(p1)
|
||||
await installPhaserDebugHook(p2)
|
||||
|
||||
const roomId = await createRoom(p1, 'Alice')
|
||||
await joinRoom(p2, 'Bob', roomId)
|
||||
await startGame(p1)
|
||||
await p1.waitForTimeout(2000)
|
||||
|
||||
const canvasWidth = await p1.evaluate(() => {
|
||||
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
||||
return game.canvas.width
|
||||
})
|
||||
const tokenX = await p1.evaluate(() => {
|
||||
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
||||
const scene = game.scene.getScene('GameScene')
|
||||
return scene.buildingTokens[0].x
|
||||
})
|
||||
// Doit être proche du bord droit (dans le dernier quart du canvas)
|
||||
expect(tokenX).toBeGreaterThan(canvasWidth * 0.75)
|
||||
|
||||
await ctx1.close()
|
||||
await ctx2.close()
|
||||
})
|
||||
|
||||
test('la carte détaillée d\'un bâtiment reste entièrement visible dans le canvas au survol', async ({ browser }) => {
|
||||
const ctx1 = await browser.newContext()
|
||||
const ctx2 = await browser.newContext()
|
||||
const p1 = await ctx1.newPage()
|
||||
const p2 = await ctx2.newPage()
|
||||
await installPhaserDebugHook(p1)
|
||||
await installPhaserDebugHook(p2)
|
||||
|
||||
const roomId = await createRoom(p1, 'Alice')
|
||||
await joinRoom(p2, 'Bob', roomId)
|
||||
await startGame(p1)
|
||||
await p1.waitForTimeout(2000)
|
||||
|
||||
const canvas = p1.locator('#phaser-container canvas')
|
||||
const box = await canvas.boundingBox()
|
||||
|
||||
const tokenPos = await p1.evaluate(() => {
|
||||
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
||||
const scene = game.scene.getScene('GameScene')
|
||||
const t = scene.buildingTokens.find(t => t.itemData.id === 'church')
|
||||
return { x: t.x, y: t.y }
|
||||
})
|
||||
|
||||
// Le survol (_scheduleShow, HOVER_SHOW_DELAY=80ms) puis le hide-timer
|
||||
// (_scheduleHide, HOVER_HIDE_DELAY=100ms) sont tous deux de courts
|
||||
// setTimeout côté navigateur ; sous charge (exécution juste après un
|
||||
// autre test dans la même suite), le timing réel peut légèrement
|
||||
// dépasser une seule attente fixe. On répète le mouvement de souris
|
||||
// jusqu'à ce que la carte soit confirmée ouverte, plutôt que de se fier
|
||||
// à un unique délai figé.
|
||||
let expandedBounds = null
|
||||
for (let attempt = 0; attempt < 5 && !expandedBounds; attempt++) {
|
||||
await p1.mouse.move(box.x + tokenPos.x, box.y + tokenPos.y + (attempt % 2)) // micro-mouvement pour re-déclencher pointerover si besoin
|
||||
await p1.waitForTimeout(300)
|
||||
expandedBounds = await p1.evaluate(() => {
|
||||
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
||||
const scene = game.scene.getScene('GameScene')
|
||||
const t = scene.buildingTokens.find(t => t.itemData.id === 'church')
|
||||
if (!t._expanded) return null
|
||||
const b = t._expanded.getBounds()
|
||||
return { x: b.x, y: b.y, right: b.right, bottom: b.bottom }
|
||||
})
|
||||
}
|
||||
expect(expandedBounds).not.toBeNull()
|
||||
|
||||
expect(expandedBounds.x).toBeGreaterThanOrEqual(0)
|
||||
expect(expandedBounds.y).toBeGreaterThanOrEqual(0)
|
||||
expect(expandedBounds.right).toBeLessThanOrEqual(box.width)
|
||||
expect(expandedBounds.bottom).toBeLessThanOrEqual(box.height)
|
||||
|
||||
await ctx1.close()
|
||||
await ctx2.close()
|
||||
})
|
||||
|
||||
test('la carte détaillée affiche le coût réparti sur les 2 jauges et la description', async ({ browser }) => {
|
||||
const ctx1 = await browser.newContext()
|
||||
const ctx2 = await browser.newContext()
|
||||
const p1 = await ctx1.newPage()
|
||||
const p2 = await ctx2.newPage()
|
||||
await installPhaserDebugHook(p1)
|
||||
await installPhaserDebugHook(p2)
|
||||
|
||||
const roomId = await createRoom(p1, 'Alice')
|
||||
await joinRoom(p2, 'Bob', roomId)
|
||||
await startGame(p1)
|
||||
await p1.waitForTimeout(2000)
|
||||
|
||||
const canvas = p1.locator('#phaser-container canvas')
|
||||
const box = await canvas.boundingBox()
|
||||
const tokenPos = await p1.evaluate(() => {
|
||||
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
||||
const scene = game.scene.getScene('GameScene')
|
||||
const t = scene.buildingTokens.find(t => t.itemData.id === 'church')
|
||||
return { x: t.x, y: t.y }
|
||||
})
|
||||
await p1.mouse.move(box.x + tokenPos.x, box.y + tokenPos.y)
|
||||
await p1.waitForTimeout(300)
|
||||
|
||||
const texts = await p1.evaluate(() => {
|
||||
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
||||
const scene = game.scene.getScene('GameScene')
|
||||
const t = scene.buildingTokens.find(t => t.itemData.id === 'church')
|
||||
return t._expanded.list.filter(o => o.type === 'Text').map(o => o.text)
|
||||
})
|
||||
|
||||
expect(texts).toContain('Église')
|
||||
expect(texts.some(t => /3 Santé.*2 Diplomatie/.test(t))).toBe(true)
|
||||
expect(texts.some(t => t.includes('Garantit +1 en Santé'))).toBe(true)
|
||||
|
||||
await ctx1.close()
|
||||
await ctx2.close()
|
||||
})
|
||||
|
||||
test('le bouton ACHETER n\'est pas interactif si UNE SEULE des 2 jauges requises est insuffisante', async ({ browser }) => {
|
||||
const ctx1 = await browser.newContext()
|
||||
const ctx2 = await browser.newContext()
|
||||
const p1 = await ctx1.newPage()
|
||||
const p2 = await ctx2.newPage()
|
||||
await installPhaserDebugHook(p1)
|
||||
await installPhaserDebugHook(p2)
|
||||
|
||||
const roomId = await createRoom(p1, 'Alice')
|
||||
await joinRoom(p2, 'Bob', roomId)
|
||||
await startGame(p1)
|
||||
await p1.waitForTimeout(2000)
|
||||
|
||||
// Jauges à 0 en tout début de partie : Église (3 santé + 2 diplo) n'est pas achetable
|
||||
const btnCenter = await hoverShopBuilding(p1, 'church')
|
||||
expect(btnCenter).toBeNull()
|
||||
|
||||
await ctx1.close()
|
||||
await ctx2.close()
|
||||
})
|
||||
|
||||
test('acheter un bâtiment le retire de la boutique pour TOUS les joueurs, débite les 2 jauges, et applique son effet', async ({ browser }) => {
|
||||
test.setTimeout(60_000)
|
||||
|
||||
const ctx1 = await browser.newContext()
|
||||
const ctx2 = await browser.newContext()
|
||||
const p1 = await ctx1.newPage()
|
||||
const p2 = await ctx2.newPage()
|
||||
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 Santé ET de Diplomatie pour Alice (coût de l'Église :
|
||||
// 3 santé + 2 diplo), pendant que Bob joue explicitement sa carte la plus
|
||||
// FAIBLE sur ces deux mêmes attributs (mode 'min') — indispensable pour
|
||||
// garantir un gain non-nul à chaque tour (gain = max(0, ma_valeur -
|
||||
// valeur_voisin_gauche)), sinon le test devient flaky selon les cartes
|
||||
// distribuées aléatoirement à chaque partie.
|
||||
let gauges = await getMyGauges(p1)
|
||||
let attempts = 0
|
||||
while ((gauges.health < 3 || gauges.diplo < 2) && attempts < 12) {
|
||||
await playFullTurn([p1, p2], [
|
||||
['health', 'diplo'],
|
||||
{ attr: ['health', 'diplo'], mode: 'min' }
|
||||
])
|
||||
gauges = await getMyGauges(p1)
|
||||
attempts++
|
||||
}
|
||||
expect(gauges.health).toBeGreaterThanOrEqual(3)
|
||||
expect(gauges.diplo).toBeGreaterThanOrEqual(2)
|
||||
|
||||
const idsBefore = await getShopBuildingIds(p1)
|
||||
expect(idsBefore).toContain('church')
|
||||
|
||||
const bought = await buyShopBuilding(p1, 'church')
|
||||
expect(bought).toBe(true)
|
||||
|
||||
// Jouer le tour pour déclencher la résolution de l'achat en attente
|
||||
await playFullTurn([p1, p2])
|
||||
|
||||
const idsAfterP1 = await getShopBuildingIds(p1)
|
||||
const idsAfterP2 = await getShopBuildingIds(p2)
|
||||
expect(idsAfterP1).not.toContain('church')
|
||||
expect(idsAfterP2).not.toContain('church')
|
||||
expect(idsAfterP1).toContain('castle') // l'autre bâtiment reste disponible
|
||||
|
||||
// Le bâtiment apparaît dans "mes bâtiments" (posés à gauche de ma main)
|
||||
const myBuildingsCount = await p1.evaluate(() => {
|
||||
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
||||
return game.scene.getScene('GameScene').myBuildingTokens.length
|
||||
})
|
||||
expect(myBuildingsCount).toBe(1)
|
||||
|
||||
// Et sous le slot d'Alice depuis la perspective de Bob (adversaire)
|
||||
const opponentBuildingsCount = await p2.evaluate(() => {
|
||||
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
||||
return game.scene.getScene('GameScene').opponentBuildingTokens.length
|
||||
})
|
||||
expect(opponentBuildingsCount).toBe(1)
|
||||
|
||||
await ctx1.close()
|
||||
await ctx2.close()
|
||||
})
|
||||
})
|
||||
+90
-11
@@ -70,12 +70,21 @@ export async function startGame(page) {
|
||||
|
||||
/**
|
||||
* Joue une carte de la main du joueur et confirme. Sélectionne dynamiquement
|
||||
* la meilleure carte pour l'attribut donné (ou la première carte si aucun
|
||||
* attribut n'est précisé), en lisant 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).
|
||||
* 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) {
|
||||
export async function playCard(page, attr = null, mode = 'max') {
|
||||
const canvas = page.locator('#phaser-container canvas')
|
||||
const box = await canvas.boundingBox()
|
||||
|
||||
@@ -86,19 +95,23 @@ export async function playCard(page, attr = null) {
|
||||
})
|
||||
if (handLen === 0) return false
|
||||
|
||||
const cardPos = await page.evaluate((attr) => {
|
||||
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 (attr) {
|
||||
let bestVal = -1
|
||||
if (attrs) {
|
||||
let bestVal = mode === 'min' ? Infinity : -1
|
||||
scene.handCards.forEach((c, i) => {
|
||||
if (c.cardData[attr] > bestVal) { bestVal = c.cardData[attr]; idx = 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 }
|
||||
}, attr)
|
||||
}, { attrs, mode })
|
||||
|
||||
await page.mouse.click(box.x + cardPos.x, box.y + cardPos.y)
|
||||
await page.waitForTimeout(200)
|
||||
@@ -118,10 +131,19 @@ export async function playCard(page, attr = null) {
|
||||
* 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++) {
|
||||
await playCard(pages[i], attrsByPage[i] ?? null)
|
||||
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)
|
||||
}
|
||||
@@ -192,3 +214,60 @@ export async function getShopItemIds(page) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -66,12 +66,14 @@ test.describe('Boutique d\'objets', () => {
|
||||
await p1.waitForTimeout(2000)
|
||||
|
||||
// Accumuler assez de guerre pour P1 en jouant sa meilleure carte de guerre
|
||||
// à chaque tour, pendant que P2 joue sur un autre attribut pour ne pas
|
||||
// rivaliser sur la jauge visée.
|
||||
// à chaque tour, pendant que P2 joue explicitement sa carte la plus
|
||||
// FAIBLE en guerre (mode 'min') — indispensable pour garantir un gain
|
||||
// non-nul à chaque tour (gain = max(0, ma_valeur - valeur_voisin_gauche)),
|
||||
// sinon le test devient flaky selon les cartes distribuées aléatoirement.
|
||||
let gauges = await getMyGauges(p1)
|
||||
let attempts = 0
|
||||
while (gauges.war < 3 && attempts < 6) {
|
||||
await playFullTurn([p1, p2], ['war', 'health'])
|
||||
while (gauges.war < 3 && attempts < 12) {
|
||||
await playFullTurn([p1, p2], ['war', { attr: 'war', mode: 'min' }])
|
||||
gauges = await getMyGauges(p1)
|
||||
attempts++
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user