- 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).
115 lines
4.2 KiB
JavaScript
115 lines
4.2 KiB
JavaScript
import { test, expect } from '@playwright/test'
|
|
import {
|
|
installPhaserDebugHook, createRoom, joinRoom, startGame,
|
|
playCard, playFullTurn, getShopItemIds, getMyGauges, buyShopItem, hoverShopItem
|
|
} from './helpers.js'
|
|
|
|
/**
|
|
* Tests d'intégration E2E : boutique d'objets (achat, effets permanents,
|
|
* disparition de la boutique une fois acheté, retrait bloqué si jauge
|
|
* insuffisante).
|
|
*/
|
|
test.describe('Boutique d\'objets', () => {
|
|
test('la boutique affiche les 4 objets 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 getShopItemIds(p1)
|
|
expect(ids.sort()).toEqual(['medkit', 'plow', 'royal_seal', 'trebuchet'].sort())
|
|
|
|
await ctx1.close()
|
|
await ctx2.close()
|
|
})
|
|
|
|
test('le bouton ACHETER n\'est pas interactif tant que la jauge 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)
|
|
|
|
// Jauge à 0 en tout début de partie : le bouton ACHETER du Trébuchet ne
|
|
// doit pas être interactif (hoverShopItem retourne null dans ce cas).
|
|
const btnCenter = await hoverShopItem(p1, 'trebuchet')
|
|
expect(btnCenter).toBeNull()
|
|
|
|
await ctx1.close()
|
|
await ctx2.close()
|
|
})
|
|
|
|
test('acheter un objet le retire de la boutique pour TOUS les joueurs, et applique son effet permanent', 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)
|
|
|
|
// Accumuler assez de guerre pour P1 en jouant sa meilleure carte de guerre
|
|
// à 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 < 12) {
|
|
await playFullTurn([p1, p2], ['war', { attr: 'war', mode: 'min' }])
|
|
gauges = await getMyGauges(p1)
|
|
attempts++
|
|
}
|
|
expect(gauges.war).toBeGreaterThanOrEqual(3)
|
|
|
|
const idsBefore = await getShopItemIds(p1)
|
|
expect(idsBefore).toContain('trebuchet')
|
|
|
|
const bought = await buyShopItem(p1, 'trebuchet')
|
|
expect(bought).toBe(true)
|
|
|
|
// Jouer le tour pour déclencher la résolution de l'achat en attente
|
|
await playFullTurn([p1, p2], ['war', 'health'])
|
|
|
|
const idsAfterP1 = await getShopItemIds(p1)
|
|
const idsAfterP2 = await getShopItemIds(p2)
|
|
expect(idsAfterP1).not.toContain('trebuchet')
|
|
expect(idsAfterP2).not.toContain('trebuchet')
|
|
expect(idsAfterP1.length).toBe(3)
|
|
|
|
// Vérifier que l'objet apparaît dans "mes objets" (posés à côté de ma main)
|
|
const myItemsCount = await p1.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
return game.scene.getScene('GameScene').myItemTokens.length
|
|
})
|
|
expect(myItemsCount).toBe(1)
|
|
|
|
// Et sous le slot de P1 depuis la perspective de P2 (adversaire)
|
|
const opponentItemsCount = await p2.evaluate(() => {
|
|
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
|
return game.scene.getScene('GameScene').opponentItemTokens.length
|
|
})
|
|
expect(opponentItemsCount).toBe(1)
|
|
|
|
await ctx1.close()
|
|
await ctx2.close()
|
|
})
|
|
})
|