Files
regnum/server/src/index.js
T
Kevin L'Hereec 1726f8f96b 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.
2026-08-21 17:05:50 +02:00

233 lines
8.9 KiB
JavaScript

const express = require('express')
const http = require('http')
const { Server } = require('socket.io')
const cors = require('cors')
const { RoomManager } = require('./game/RoomManager')
const app = express()
app.use(cors())
app.use(express.json())
// Route de santé minimale : utilisée notamment par le healthcheck HTTP de
// Playwright (config e2e/playwright.config.js, webServer.url) pour détecter
// qu'un serveur est déjà démarré et éviter un conflit de port (EADDRINUSE)
// en local. Sans route HTTP définie, toute requête renvoyait 404, que
// Playwright ne considère pas comme "serveur prêt".
app.get('/health', (req, res) => res.status(200).json({ status: 'ok' }))
const server = http.createServer(app)
const io = new Server(server, {
cors: { origin: '*', methods: ['GET', 'POST'] }
})
const rooms = new RoomManager()
io.on('connection', (socket) => {
console.log(`[+] ${socket.id}`)
// ─── Rejoindre une salle ─────────────────────────────────────────────────────
socket.on('join-room', ({ roomId, playerName }) => {
const room = rooms.getOrCreate(roomId)
const joined = room.addPlayer(socket.id, playerName)
if (!joined) {
socket.emit('error', { message: 'Salle pleine (4 joueurs max)' })
return
}
socket.join(roomId)
socket.data.roomId = roomId
socket.data.playerName = playerName
io.to(roomId).emit('room-update', { players: room.getPlayersInfo() })
console.log(`[${roomId}] ${playerName} rejoint (${room.playerCount}/4)`)
})
// ─── Démarrer la partie ──────────────────────────────────────────────────────
socket.on('start-game', ({ roomId }) => {
const room = rooms.get(roomId)
if (!room || room.playerCount < 2) return
room.startGame()
io.to(roomId).emit('game-start')
// Envoyer la main personnalisée à chaque joueur
room.players.forEach(player => {
io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId))
})
console.log(`[${roomId}] Partie démarrée`)
})
// ─── Demander l'état courant ─────────────────────────────────────────────────
socket.on('request-state', ({ roomId }) => {
const room = rooms.get(roomId)
if (!room) return
socket.emit('game-state', room.getStateFor(socket.id))
})
// ─── Acheter un objet ────────────────────────────────────────────────────────
socket.on('buy-item', ({ roomId, itemId }) => {
const room = rooms.get(roomId)
if (!room) return
const result = room.buyItem(socket.id, itemId)
if (!result.ok) {
socket.emit('error', { message: result.error })
return
}
// L'achat est différé (résolu avec le tour) : on renvoie juste l'état à
// jour à chaque joueur pour refléter la tentative en attente.
room.players.forEach(player => {
io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId))
})
})
// ─── Acheter un bâtiment ─────────────────────────────────────────────────────
socket.on('buy-building', ({ roomId, buildingId }) => {
const room = rooms.get(roomId)
if (!room) return
const result = room.buyBuilding(socket.id, buildingId)
if (!result.ok) {
socket.emit('error', { message: result.error })
return
}
room.players.forEach(player => {
io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId))
})
})
// ─── Piocher une carte Événement ─────────────────────────────────────────────
socket.on('buy-event-draw', ({ roomId }) => {
const room = rooms.get(roomId)
if (!room) return
const result = room.buyEventDraw(socket.id)
if (!result.ok) {
socket.emit('error', { message: result.error })
return
}
// Contrairement aux objets/bâtiments, la pioche est immédiate (pas de
// conflit de stock possible) : chaque joueur reçoit son état à jour tout
// de suite (seul le joueur ayant pioché voit la nouvelle carte dans
// myEventHand, les autres voient juste ses jauges diminuer).
room.players.forEach(player => {
io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId))
})
})
// ─── Choisir de jouer une carte Événement ce tour ────────────────────────────
socket.on('play-event', ({ roomId, eventId, targets }) => {
const room = rooms.get(roomId)
if (!room) return
const result = room.playEventCard(socket.id, eventId, targets)
if (!result.ok) {
socket.emit('error', { message: result.error })
return
}
room.players.forEach(player => {
io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId))
})
})
// ─── Jouer une carte ─────────────────────────────────────────────────────────
socket.on('play-card', ({ roomId, cardId }) => {
const room = rooms.get(roomId)
if (!room) return
const result = room.playCard(socket.id, cardId)
if (!result.ok) {
socket.emit('error', { message: result.error })
return
}
// Broadcaster le compteur de joueurs prêts à tous
io.to(roomId).emit('players-ready', {
ready: room.pendingPlays.size,
total: room.playerCount
})
// Envoyer à chaque joueur son état personnalisé :
// - celui qui vient de jouer reçoit phase='waiting'
// - les autres restent en phase='selecting'
room.players.forEach(player => {
io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId))
})
// Tous les joueurs ont joué → résolution automatique
if (room.allPlayersPlayed()) {
const roundResult = room.resolveRound()
console.log(`[${roomId}] Tour ${room.currentTurn} résolu`)
// Révéler les cartes jouées, les gains par attribut, les objets/bâtiments
// achetés, et les résultats des cartes Événement jouées ce tour.
io.to(roomId).emit('round-result', {
playedCards: roundResult.playedCards,
gains: roundResult.gains,
boughtItems: roundResult.boughtItems,
boughtBuildings: roundResult.boughtBuildings,
eventResults: roundResult.eventResults,
message: roundResult.message
})
// Mise à jour scores (état personnalisé par joueur : items, achat en
// attente, objets encore disponibles, etc.)
room.players.forEach(player => {
io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId))
})
// 3 secondes d'affichage puis tour suivant / fin
setTimeout(() => {
room.nextTurn()
if (room.isGameOver()) {
io.to(roomId).emit('game-over', room.getPlayersInfo())
console.log(`[${roomId}] Partie terminée`)
} else {
// Distribuer la nouvelle main à chaque joueur
room.players.forEach(player => {
io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId))
})
}
}, 3500)
}
})
// ─── Quitter une salle explicitement (bouton retour) ────────────────────────
socket.on('leave-room', ({ roomId }) => {
const room = rooms.get(roomId)
if (room) {
room.removePlayer(socket.id)
socket.leave(roomId)
io.to(roomId).emit('room-update', { players: room.getPlayersInfo() })
if (room.playerCount === 0) rooms.delete(roomId)
console.log(`[${roomId}] ${socket.data.playerName ?? socket.id} a quitté`)
}
socket.data.roomId = null
})
// ─── Déconnexion ─────────────────────────────────────────────────────────────
socket.on('disconnect', () => {
const roomId = socket.data.roomId
if (roomId) {
const room = rooms.get(roomId)
if (room) {
room.removePlayer(socket.id)
io.to(roomId).emit('room-update', { players: room.getPlayersInfo() })
if (room.playerCount === 0) rooms.delete(roomId)
}
}
console.log(`[-] ${socket.id}`)
})
})
const PORT = process.env.PORT || 3000
server.listen(PORT, () => {
console.log(`Regnum server → http://localhost:${PORT}`)
})