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
+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)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user