feat: cartes Événement (pioche partagée de 18 cartes, 6 effets, synergies, ciblage)
- Nouveau server/src/game/events.js : 6 événements (Récolte exceptionnelle, Épidémie, Levée des troupes, Accord royal, Complot découvert, Grande fête au château), deck PARTAGÉ de 18 cartes (3 exemplaires de chacun) mélangé au démarrage de la partie. - Room.js : buyEventDraw() (coût fixe 1 point dans chacune des 4 jauges, effet immédiat contrairement aux objets/bâtiments — pas de conflit de stock possible), playEventCard() (choix de jouer une carte de sa main, max 1 par tour, avec ciblage optionnel), _resolvePendingEventPlays() (résolution de TOUS les événements du tour AVANT le calcul des gains de cartes personnage, défausse systématique, protection "Complot découvert" pour soi-même uniquement). Les 6 effets incluent leurs synergies avec les objets/bâtiments existants (Charrue, Trousse+Église pour Épidémie, Trébuchet, Sceau Royal, Château Royal → pioche bonus). - IMPORTANT : le ciblage de joueur (Accord royal, variante Trousse d'Épidémie) transite par teamIndex (identifiant public) et non socketId (jamais exposé au client via getPlayersInfo/getStateFor) — le serveur résout teamIndex -> joueur réel au moment de la résolution. - Nouvel event socket 'buy-event-draw' (effet immédiat) et 'play-event' (résolution différée), eventResults inclus dans round-result. - Client : events.js (miroir), EventCardObject.js (carte épurée façon CardObject, badge rouge "!" pour les événements à effet négatif). BootScene génère les textures plein cadre (méthode _generateClippedTexture généralisée, réutilisée avec les personnages). - GameScene : bouton de pioche à droite du bouton de confirmation (état grisé selon jauges/main pleine/pioche déjà tentée), seconde main de cartes événement (max 2) au-dessus de ce bouton, sélection d'une carte pour la jouer avec séquence de ciblage interactive (surbrillance des slots adversaires cliquables) pour Accord royal et la variante Trousse d'Épidémie. - 61 nouveaux tests unitaires serveur (events.js + Room.js : pioche, choix de jouer, résolution des 6 effets avec/sans synergie, Complot découvert, ordre événements-avant-personnages, deck partagé). - Nouveaux helpers et tests d'intégration E2E Playwright (e2e/tests/events.spec.js) pour la pioche, la limite de main à 2, et les 6 effets — certains scénarios restent probabilistes (pioche aléatoire dans le deck partagé) et peuvent nécessiter une stabilisation supplémentaire du timing.
This commit is contained in:
1 parent
9139989fa7
commit
1726f8f96b
17 files changed
+1952
-16
No files matched your search
@@ -271,3 +271,109 @@ export async function getShopBuildingIds(page) {
|
||||
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. */
|
||||
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 = 12) {
|
||||
for (let attempt = 0; attempt < maxDraws; attempt++) {
|
||||
const hand = await getMyEventHand(page)
|
||||
if (hand.includes(eventId)) return true
|
||||
if (hand.length >= 2) return false // main pleine sans l'event recherché : abandon
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sélectionne (clique) une carte de ma main d'événements pour la jouer ce
|
||||
* tour. 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.
|
||||
*/
|
||||
export async function playEventCard(page, eventId, targetTeamIndexes = []) {
|
||||
const canvas = page.locator('#phaser-container canvas')
|
||||
const box = await canvas.boundingBox()
|
||||
|
||||
const cardPos = await page.evaluate((eventId) => {
|
||||
const game = window.__ALL_GAMES__[window.__ALL_GAMES__.length - 1]
|
||||
const scene = game.scene.getScene('GameScene')
|
||||
const card = scene.eventCards.find(c => c.eventData.id === eventId)
|
||||
if (!card) return null
|
||||
const b = card.getBounds()
|
||||
return { x: b.centerX, y: b.centerY }
|
||||
}, eventId)
|
||||
if (!cardPos) return false
|
||||
|
||||
await page.mouse.click(box.x + cardPos.x, box.y + cardPos.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)
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user