feat: gauge scoring system (subtract left neighbor per attribute)
This commit is contained in:
1 parent
25ab76c4ac
commit
91a1a9f4f0
5 files changed
+297
-203
No files matched your search
@@ -17,12 +17,6 @@ const ATTR_COLORS = {
|
||||
diplo: 0x3399ee
|
||||
}
|
||||
|
||||
const ATTR_LABELS = {
|
||||
food: 'Nourriture',
|
||||
war: 'Guerre',
|
||||
health: 'Santé',
|
||||
diplo: 'Diplomatie'
|
||||
}
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
@@ -184,104 +178,96 @@ export class GameScene extends Phaser.Scene {
|
||||
|
||||
// ─── Résolution visuelle ─────────────────────────────────────────────────────
|
||||
|
||||
_showPlayedCards(playedCards) {
|
||||
// Nettoyer
|
||||
/**
|
||||
* Affiche les cartes jouées et les gains obtenus (soustraction voisin gauche)
|
||||
* @param {Array} playedCards - cartes jouées dans l'ordre des joueurs
|
||||
* @param {Array} gains - gains[i] = { food, war, health, diplo } pour le joueur i
|
||||
*/
|
||||
_showPlayedCards(playedCards, gains = []) {
|
||||
this.resolveObjects.forEach(o => o.destroy())
|
||||
this.resolveObjects = []
|
||||
|
||||
const ATTRS_POS = [
|
||||
{ key: 'food', label: 'NOU', dx: -28, dy: -20 },
|
||||
{ key: 'war', label: 'GUE', dx: 28, dy: -20 },
|
||||
{ key: 'health', label: 'SAN', dx: -28, dy: 30 },
|
||||
{ key: 'diplo', label: 'DIP', dx: 28, dy: 30 }
|
||||
]
|
||||
|
||||
playedCards.forEach((entry, i) => {
|
||||
const slot = this.playSlots[i]
|
||||
if (!slot) return
|
||||
|
||||
const teamColor = TEAM_COLORS[entry.teamIndex] ?? 0x888888
|
||||
const gain = gains[i] ?? {}
|
||||
|
||||
// Mini carte de résolution
|
||||
// ── Fond carte ──────────────────────────────────────────
|
||||
const g = this.add.graphics()
|
||||
g.fillStyle(this._darken(teamColor, 0.3))
|
||||
g.fillRoundedRect(-55, -85, 110, 170, 6)
|
||||
g.fillStyle(this._darken(teamColor, 0.25))
|
||||
g.fillRoundedRect(-55, -90, 110, 180, 6)
|
||||
g.lineStyle(2, teamColor)
|
||||
g.strokeRoundedRect(-55, -85, 110, 170, 6)
|
||||
g.strokeRoundedRect(-55, -90, 110, 180, 6)
|
||||
g.x = slot.x
|
||||
g.y = slot.y
|
||||
this.resolveObjects.push(g)
|
||||
|
||||
// Nom du joueur
|
||||
const pName = this.add.text(slot.x, slot.y - 72, entry.playerName, {
|
||||
fontSize: '10px', fontFamily: 'Georgia', color: '#ffffff', fontStyle: 'bold'
|
||||
}).setOrigin(0.5)
|
||||
this.resolveObjects.push(pName)
|
||||
|
||||
// Nom de la carte
|
||||
const cName = this.add.text(slot.x, slot.y - 56, entry.cardName, {
|
||||
fontSize: '13px', fontFamily: 'Georgia', color: '#d4a732', fontStyle: 'bold'
|
||||
}).setOrigin(0.5)
|
||||
this.resolveObjects.push(cName)
|
||||
|
||||
// 4 attributs
|
||||
const attrs = [
|
||||
{ key: 'food', label: 'NOU', cx: slot.x - 28, cy: slot.y - 20 },
|
||||
{ key: 'war', label: 'GUE', cx: slot.x + 28, cy: slot.y - 20 },
|
||||
{ key: 'health', label: 'SAN', cx: slot.x - 28, cy: slot.y + 40 },
|
||||
{ key: 'diplo', label: 'DIP', cx: slot.x + 28, cy: slot.y + 40 }
|
||||
]
|
||||
|
||||
attrs.forEach(({ key, label, cx, cy }) => {
|
||||
const c = this.add.graphics()
|
||||
c.fillStyle(ATTR_COLORS[key], 0.85)
|
||||
c.fillRoundedRect(-20, -18, 40, 36, 4)
|
||||
c.x = cx
|
||||
c.y = cy
|
||||
this.resolveObjects.push(c)
|
||||
|
||||
const v = this.add.text(cx, cy - 4, String(entry[key]), {
|
||||
fontSize: '14px', fontFamily: 'Georgia', fontStyle: 'bold',
|
||||
color: '#ffffff', stroke: '#000000', strokeThickness: 2
|
||||
// ── Nom du joueur ────────────────────────────────────────
|
||||
this.resolveObjects.push(
|
||||
this.add.text(slot.x, slot.y - 78, entry.playerName, {
|
||||
fontSize: '10px', fontFamily: 'Georgia', color: '#ffffff', fontStyle: 'bold'
|
||||
}).setOrigin(0.5)
|
||||
this.resolveObjects.push(v)
|
||||
)
|
||||
|
||||
const l = this.add.text(cx, cy + 10, label, {
|
||||
fontSize: '7px', fontFamily: 'Arial', color: '#ffffff'
|
||||
// ── Nom de la carte ──────────────────────────────────────
|
||||
this.resolveObjects.push(
|
||||
this.add.text(slot.x, slot.y - 62, entry.cardName, {
|
||||
fontSize: '12px', fontFamily: 'Georgia', color: '#d4a732', fontStyle: 'bold'
|
||||
}).setOrigin(0.5)
|
||||
this.resolveObjects.push(l)
|
||||
)
|
||||
|
||||
// ── 4 badges attribut + gain ─────────────────────────────
|
||||
ATTRS_POS.forEach(({ key, label, dx, dy }) => {
|
||||
const cx = slot.x + dx
|
||||
const cy = slot.y + dy
|
||||
const cardVal = entry[key] ?? 0
|
||||
const gainVal = gain[key] ?? 0
|
||||
|
||||
// Badge fond coloré
|
||||
const badge = this.add.graphics()
|
||||
badge.fillStyle(ATTR_COLORS[key], gainVal > 0 ? 1.0 : 0.45)
|
||||
badge.fillRoundedRect(-22, -20, 44, 40, 4)
|
||||
badge.x = cx
|
||||
badge.y = cy
|
||||
this.resolveObjects.push(badge)
|
||||
|
||||
// Valeur de la carte
|
||||
this.resolveObjects.push(
|
||||
this.add.text(cx, cy - 7, String(cardVal), {
|
||||
fontSize: '14px', fontFamily: 'Georgia', fontStyle: 'bold',
|
||||
color: '#ffffff', stroke: '#000000', strokeThickness: 2
|
||||
}).setOrigin(0.5)
|
||||
)
|
||||
|
||||
// Label abrégé
|
||||
this.resolveObjects.push(
|
||||
this.add.text(cx, cy + 5, label, {
|
||||
fontSize: '7px', fontFamily: 'Arial', color: '#ffffffcc'
|
||||
}).setOrigin(0.5)
|
||||
)
|
||||
|
||||
// Gain (affiché en vert si > 0)
|
||||
if (gainVal > 0) {
|
||||
this.resolveObjects.push(
|
||||
this.add.text(cx, cy + 16, `+${gainVal}`, {
|
||||
fontSize: '10px', fontFamily: 'Georgia', fontStyle: 'bold',
|
||||
color: '#ffffff', stroke: '#000000', strokeThickness: 2
|
||||
}).setOrigin(0.5)
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_showAttrResults(attrResults) {
|
||||
const { width, height } = this.scale
|
||||
const attrs = ['food', 'war', 'health', 'diplo']
|
||||
const boxY = height / 2 + 80
|
||||
|
||||
attrs.forEach((attr, i) => {
|
||||
const r = attrResults[attr]
|
||||
const x = width / 2 - 300 + i * 200
|
||||
|
||||
const g = this.add.graphics()
|
||||
g.fillStyle(ATTR_COLORS[attr], 0.15)
|
||||
g.fillRoundedRect(-90, -20, 180, 40, 6)
|
||||
g.lineStyle(1, ATTR_COLORS[attr], 0.6)
|
||||
g.strokeRoundedRect(-90, -20, 180, 40, 6)
|
||||
g.x = x
|
||||
g.y = boxY
|
||||
this.resolveObjects.push(g)
|
||||
|
||||
const label = ATTR_LABELS[attr]
|
||||
const winText = r.winner === 'tie'
|
||||
? `Égalité`
|
||||
: `${r.winner} +${r.value}`
|
||||
|
||||
const t1 = this.add.text(x, boxY - 8, label, {
|
||||
fontSize: '11px', fontFamily: 'Georgia', color: '#' + ATTR_COLORS[attr].toString(16).padStart(6, '0')
|
||||
}).setOrigin(0.5)
|
||||
this.resolveObjects.push(t1)
|
||||
|
||||
const t2 = this.add.text(x, boxY + 8, winText, {
|
||||
fontSize: '12px', fontFamily: 'Georgia', color: '#ffffff', fontStyle: 'bold'
|
||||
}).setOrigin(0.5)
|
||||
this.resolveObjects.push(t2)
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Socket listeners ────────────────────────────────────────────────────────
|
||||
|
||||
_setupSocketListeners() {
|
||||
@@ -307,10 +293,9 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
})
|
||||
|
||||
this.socket.on('round-result', ({ playedCards, attrResults }) => {
|
||||
this.socket.on('round-result', ({ playedCards, gains }) => {
|
||||
this.phaseText.setText('Résolution du tour...')
|
||||
this._showPlayedCards(playedCards)
|
||||
this._showAttrResults(attrResults)
|
||||
this._showPlayedCards(playedCards, gains)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+16
-21
@@ -2,13 +2,13 @@ import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useGameStore = defineStore('game', () => {
|
||||
const playerName = ref('')
|
||||
const playerId = ref('')
|
||||
const players = ref([])
|
||||
const currentTurn = ref(0)
|
||||
const maxTurns = ref(10)
|
||||
const phase = ref('idle') // idle | selecting | waiting | resolve | end
|
||||
const hand = ref([])
|
||||
const playerName = ref('')
|
||||
const players = ref([]) // { name, teamIndex, gauges:{food,war,health,diplo}, score, handSize }
|
||||
const currentTurn = ref(0)
|
||||
const maxTurns = ref(12)
|
||||
const phase = ref('idle')
|
||||
const hand = ref([])
|
||||
const myTeamIndex = ref(0)
|
||||
const selectedCard = ref(null)
|
||||
|
||||
function setPlayer(name) {
|
||||
@@ -16,25 +16,20 @@ export const useGameStore = defineStore('game', () => {
|
||||
}
|
||||
|
||||
function updateState(state) {
|
||||
players.value = state.players
|
||||
players.value = state.players
|
||||
currentTurn.value = state.currentTurn
|
||||
maxTurns.value = state.maxTurns
|
||||
phase.value = state.phase
|
||||
hand.value = state.hand || hand.value
|
||||
maxTurns.value = state.maxTurns
|
||||
phase.value = state.phase
|
||||
myTeamIndex.value = state.myTeamIndex ?? 0
|
||||
if (state.hand !== undefined) hand.value = state.hand
|
||||
}
|
||||
|
||||
function selectCard(card) {
|
||||
selectedCard.value = card
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedCard.value = null
|
||||
}
|
||||
function selectCard(card) { selectedCard.value = card }
|
||||
function clearSelection() { selectedCard.value = null }
|
||||
|
||||
return {
|
||||
playerName, playerId,
|
||||
players, currentTurn, maxTurns,
|
||||
phase, hand, selectedCard,
|
||||
playerName, players, currentTurn, maxTurns,
|
||||
phase, hand, myTeamIndex, selectedCard,
|
||||
setPlayer, updateState, selectCard, clearSelection
|
||||
}
|
||||
})
|
||||
+149
-26
@@ -1,21 +1,41 @@
|
||||
<template>
|
||||
<div class="game-wrapper">
|
||||
<!-- UI Vue: scores et infos -->
|
||||
|
||||
<!-- HUD : scores de tous les joueurs -->
|
||||
<div class="hud">
|
||||
<div class="scores">
|
||||
<div v-for="(player, i) in gameStore.players" :key="i" class="score-card">
|
||||
<span class="pname">{{ player.name }}</span>
|
||||
<span class="pts">{{ player.score }} pts</span>
|
||||
<div
|
||||
v-for="(player, i) in gameStore.players"
|
||||
:key="i"
|
||||
class="player-panel"
|
||||
:style="{ borderColor: teamCss[player.teamIndex] }"
|
||||
>
|
||||
<div class="player-name" :style="{ color: teamCss[player.teamIndex] }">
|
||||
{{ player.name }}
|
||||
</div>
|
||||
<div class="gauges">
|
||||
<div class="gauge-row" v-for="attr in ATTRS" :key="attr">
|
||||
<span class="gauge-icon">{{ attrIcon[attr] }}</span>
|
||||
<div class="gauge-bar-bg">
|
||||
<div
|
||||
class="gauge-bar-fill"
|
||||
:style="{
|
||||
width: gaugePercent(player.gauges?.[attr] ?? 0) + '%',
|
||||
background: attrColor[attr]
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<span class="gauge-val">{{ player.gauges?.[attr] ?? 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="total-score">{{ player.score ?? 0 }} pts</div>
|
||||
</div>
|
||||
|
||||
<div class="turn-info">
|
||||
Tour <strong>{{ gameStore.currentTurn }}</strong> / {{ gameStore.maxTurns }}
|
||||
Tour <strong>{{ gameStore.currentTurn }}</strong>/{{ gameStore.maxTurns }}
|
||||
</div>
|
||||
<div v-if="gameStore.phase === 'waiting'" class="phase-label">
|
||||
En attente des autres joueurs...
|
||||
</div>
|
||||
<div v-else-if="gameStore.phase === 'resolve'" class="phase-label">
|
||||
Résolution du tour...
|
||||
|
||||
<div class="phase-label" :class="gameStore.phase">
|
||||
{{ phaseLabel[gameStore.phase] ?? '' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +57,34 @@ const socket = useSocket()
|
||||
const phaserContainer = ref(null)
|
||||
let phaserGame = null
|
||||
|
||||
const ATTRS = ['food', 'war', 'health', 'diplo']
|
||||
|
||||
const attrIcon = { food: '🟡', war: '🔴', health: '🟢', diplo: '🔵' }
|
||||
const attrColor = {
|
||||
food: '#f0c030',
|
||||
war: '#dd2222',
|
||||
health: '#22aa44',
|
||||
diplo: '#3399ee'
|
||||
}
|
||||
const teamCss = {
|
||||
0: '#cc2200',
|
||||
1: '#1155cc',
|
||||
2: '#1a7a2e',
|
||||
3: '#b8860b'
|
||||
}
|
||||
const phaseLabel = {
|
||||
selecting: 'Sélectionnez une carte',
|
||||
waiting: 'En attente des autres...',
|
||||
resolve: 'Résolution du tour...',
|
||||
end: 'Partie terminée !'
|
||||
}
|
||||
|
||||
// Max théorique d'une jauge pour la barre (sum max attr * maxTurns)
|
||||
function gaugePercent(val) {
|
||||
const max = 6 * 12 // valeur max théorique d'une jauge
|
||||
return Math.min(100, Math.round((val / max) * 100))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
phaserGame = createGame('phaser-container', socket, gameStore, route.params.roomId)
|
||||
|
||||
@@ -58,26 +106,101 @@ onUnmounted(() => {
|
||||
height: 100vh;
|
||||
background: #0d0500;
|
||||
}
|
||||
|
||||
/* ── HUD ───────────────────────────────────────────────────── */
|
||||
.hud {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #1a0a00;
|
||||
border-bottom: 1px solid #d4a73240;
|
||||
gap: 1rem;
|
||||
padding: 6px 12px;
|
||||
background: #120700;
|
||||
border-bottom: 1px solid #d4a73230;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.scores { display: flex; gap: 1rem; }
|
||||
.score-card {
|
||||
|
||||
.player-panel {
|
||||
border: 1px solid #4a3020;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
min-width: 160px;
|
||||
background: #1a0a0080;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
font-family: Georgia, serif;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.gauges {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.25rem 1rem;
|
||||
border: 1px solid #d4a73250;
|
||||
border-radius: 4px;
|
||||
gap: 3px;
|
||||
}
|
||||
.pname { font-size: 0.75rem; color: #a08060; }
|
||||
.pts { font-size: 1.1rem; font-weight: bold; color: #d4a732; }
|
||||
.turn-info { margin-left: auto; color: #f5e6c8; }
|
||||
.phase-label { color: #d4a732; font-style: italic; }
|
||||
#phaser-container { flex: 1; }
|
||||
|
||||
.gauge-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.gauge-icon { font-size: 10px; width: 16px; }
|
||||
|
||||
.gauge-bar-bg {
|
||||
flex: 1;
|
||||
height: 7px;
|
||||
background: #2a1500;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gauge-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.gauge-val {
|
||||
font-size: 10px;
|
||||
color: #f5e6c8;
|
||||
width: 20px;
|
||||
text-align: right;
|
||||
font-family: Georgia, serif;
|
||||
}
|
||||
|
||||
.total-score {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #d4a732;
|
||||
font-family: Georgia, serif;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.turn-info {
|
||||
margin-left: auto;
|
||||
color: #f5e6c8;
|
||||
font-family: Georgia, serif;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.phase-label {
|
||||
font-size: 12px;
|
||||
font-family: Georgia, serif;
|
||||
font-style: italic;
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.phase-label.selecting { color: #d4a732; }
|
||||
.phase-label.waiting { color: #a08060; }
|
||||
.phase-label.resolve { color: #22aa44; }
|
||||
.phase-label.end { color: #cc2200; }
|
||||
|
||||
/* ── Canvas ─────────────────────────────────────────────────── */
|
||||
#phaser-container { flex: 1; overflow: hidden; }
|
||||
</style>
|
||||
+60
-69
@@ -1,26 +1,27 @@
|
||||
const { dealHands } = require('./cards')
|
||||
|
||||
const MAX_PLAYERS = 4
|
||||
const MAX_TURNS = 12 // 12 tours (1 par personnage potentiellement)
|
||||
const HAND_SIZE = 4 // 4 cartes en main au départ
|
||||
const MAX_TURNS = 12
|
||||
const HAND_SIZE = 4
|
||||
const ATTRS = ['food', 'war', 'health', 'diplo']
|
||||
|
||||
/**
|
||||
* Logique de résolution par tour :
|
||||
* - Chaque joueur joue une carte simultanément
|
||||
* - On compare les 4 attributs (food, war, health, diplo) entre tous les joueurs
|
||||
* - Pour chaque attribut, le joueur avec la valeur la plus élevée marque des points
|
||||
* - En cas d'égalité, personne ne marque sur cet attribut
|
||||
* - Les points marqués = valeur de l'attribut gagnant
|
||||
* Système de score :
|
||||
* Chaque joueur possède 4 jauges (food, war, health, diplo), toutes à 0 au départ.
|
||||
* À chaque tour, on compare chaque joueur à son VOISIN DE GAUCHE (sens horaire) :
|
||||
* gain = max(0, ma_valeur_attr - valeur_attr_voisin_gauche)
|
||||
* Ce gain s'ajoute à la jauge correspondante du joueur.
|
||||
* Le score total est la somme des 4 jauges.
|
||||
*/
|
||||
|
||||
class Room {
|
||||
constructor(id) {
|
||||
this.id = id
|
||||
this.players = [] // { socketId, name, teamIndex, score, hand }
|
||||
this.phase = 'lobby' // lobby | selecting | waiting | resolve | end
|
||||
this.id = id
|
||||
this.players = [] // { socketId, name, teamIndex, gauges:{food,war,health,diplo}, hand }
|
||||
this.phase = 'lobby' // lobby | selecting | waiting | resolve | end
|
||||
this.currentTurn = 0
|
||||
this.maxTurns = MAX_TURNS
|
||||
this.pendingPlays = new Map() // socketId -> card
|
||||
this.maxTurns = MAX_TURNS
|
||||
this.pendingPlays = new Map() // socketId -> { player, card }
|
||||
}
|
||||
|
||||
get playerCount() { return this.players.length }
|
||||
@@ -34,8 +35,8 @@ class Room {
|
||||
this.players.push({
|
||||
socketId,
|
||||
name,
|
||||
teamIndex: this.players.length, // 0=Écarlate 1=Azur 2=Émeraude 3=Or
|
||||
score: 0,
|
||||
teamIndex: this.players.length,
|
||||
gauges: { food: 0, war: 0, health: 0, diplo: 0 },
|
||||
hand: []
|
||||
})
|
||||
return true
|
||||
@@ -49,7 +50,10 @@ class Room {
|
||||
|
||||
startGame() {
|
||||
const hands = dealHands(this.players.length, HAND_SIZE)
|
||||
this.players.forEach((p, i) => { p.hand = hands[i] })
|
||||
this.players.forEach((p, i) => {
|
||||
p.hand = hands[i]
|
||||
p.gauges = { food: 0, war: 0, health: 0, diplo: 0 }
|
||||
})
|
||||
this.currentTurn = 1
|
||||
this.phase = 'selecting'
|
||||
this.pendingPlays.clear()
|
||||
@@ -72,8 +76,6 @@ class Room {
|
||||
const [card] = player.hand.splice(cardIndex, 1)
|
||||
this.pendingPlays.set(socketId, { player, card })
|
||||
|
||||
// La phase globale reste 'selecting' tant que tout le monde n'a pas joué
|
||||
// (seul le joueur qui vient de jouer passe en 'waiting' côté client via son état perso)
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
@@ -84,82 +86,73 @@ class Room {
|
||||
// ─── Résolution du tour ─────────────────────────────────────────────────────
|
||||
|
||||
resolveRound() {
|
||||
const plays = [...this.pendingPlays.values()]
|
||||
const n = this.players.length
|
||||
// Construire un tableau ordonné des cartes jouées (même ordre que this.players)
|
||||
const plays = this.players.map(p => ({
|
||||
player: p,
|
||||
card: this.pendingPlays.get(p.socketId)?.card ?? null
|
||||
}))
|
||||
|
||||
// Cartes jouées (pour affichage)
|
||||
// Cartes jouées pour l'affichage
|
||||
const playedCards = plays.map(({ player, card }) => ({
|
||||
playerName: player.name,
|
||||
teamIndex: player.teamIndex,
|
||||
cardId: card.id,
|
||||
cardName: card.name,
|
||||
food: card.food,
|
||||
war: card.war,
|
||||
health: card.health,
|
||||
diplo: card.diplo
|
||||
cardId: card?.id,
|
||||
cardName: card?.name,
|
||||
food: card?.food ?? 0,
|
||||
war: card?.war ?? 0,
|
||||
health: card?.health ?? 0,
|
||||
diplo: card?.diplo ?? 0
|
||||
}))
|
||||
|
||||
// Résolution attribut par attribut
|
||||
const attrs = ['food', 'war', 'health', 'diplo']
|
||||
const pointsGained = {} // socketId -> { food, war, health, diplo, total }
|
||||
plays.forEach(({ player }) => {
|
||||
pointsGained[player.socketId] = { food: 0, war: 0, health: 0, diplo: 0, total: 0 }
|
||||
// Gains par joueur et par attribut
|
||||
// Règle : je compare mon attribut à celui du VOISIN DE GAUCHE (index - 1, modulo n)
|
||||
// gain = max(0, moi - voisin_gauche)
|
||||
const gains = plays.map((play, i) => {
|
||||
const leftIndex = (i - 1 + n) % n
|
||||
const leftCard = plays[leftIndex].card
|
||||
|
||||
const g = {}
|
||||
ATTRS.forEach(attr => {
|
||||
g[attr] = Math.max(0, (play.card?.[attr] ?? 0) - (leftCard?.[attr] ?? 0))
|
||||
})
|
||||
return g
|
||||
})
|
||||
|
||||
const attrResults = {} // attr -> { winner: name | 'tie', value }
|
||||
|
||||
attrs.forEach(attr => {
|
||||
const max = Math.max(...plays.map(p => p.card[attr]))
|
||||
const winners = plays.filter(p => p.card[attr] === max)
|
||||
|
||||
if (winners.length === 1) {
|
||||
// Un seul gagnant
|
||||
const { player } = winners[0]
|
||||
pointsGained[player.socketId][attr] = max
|
||||
pointsGained[player.socketId].total += max
|
||||
player.score += max
|
||||
attrResults[attr] = { winner: player.name, value: max }
|
||||
} else {
|
||||
// Égalité → personne ne marque
|
||||
attrResults[attr] = { winner: 'tie', value: max, tiedPlayers: winners.map(w => w.player.name) }
|
||||
}
|
||||
// Appliquer les gains aux jauges
|
||||
plays.forEach(({ player }, i) => {
|
||||
ATTRS.forEach(attr => {
|
||||
player.gauges[attr] += gains[i][attr]
|
||||
})
|
||||
})
|
||||
|
||||
// Message de résumé
|
||||
const lines = attrs.map(attr => {
|
||||
const r = attrResults[attr]
|
||||
const label = { food: 'Nourriture', war: 'Guerre', health: 'Santé', diplo: 'Diplomatie' }[attr]
|
||||
if (r.winner === 'tie') return `${label} : Égalité (${r.tiedPlayers.join(' & ')}) — personne ne marque`
|
||||
return `${label} : ${r.winner} gagne ${r.value} pts`
|
||||
// Résumé textuel
|
||||
const lines = plays.map(({ player }, i) => {
|
||||
const g = gains[i]
|
||||
const parts = ATTRS
|
||||
.filter(a => g[a] > 0)
|
||||
.map(a => `+${g[a]} ${a}`)
|
||||
return `${player.name} : ${parts.length ? parts.join(', ') : 'rien'}`
|
||||
})
|
||||
|
||||
this.pendingPlays.clear()
|
||||
this.phase = 'resolve'
|
||||
|
||||
return {
|
||||
playedCards,
|
||||
attrResults,
|
||||
pointsGained,
|
||||
message: lines.join('\n')
|
||||
}
|
||||
return { playedCards, gains, message: lines.join(' | ') }
|
||||
}
|
||||
|
||||
// ─── Tour suivant ────────────────────────────────────────────────────────────
|
||||
|
||||
nextTurn() {
|
||||
this.currentTurn++
|
||||
|
||||
if (this.isGameOver()) {
|
||||
this.phase = 'end'
|
||||
return
|
||||
}
|
||||
|
||||
this.phase = 'selecting'
|
||||
// Si un joueur n'a plus de cartes en main, on lui repioche depuis son deck perso
|
||||
// (dans cette version simple la main se vide naturellement)
|
||||
}
|
||||
|
||||
isGameOver() {
|
||||
// Partie terminée quand tous les joueurs n'ont plus de cartes OU après maxTurns
|
||||
const allEmpty = this.players.every(p => p.hand.length === 0)
|
||||
return allEmpty || this.currentTurn >= this.maxTurns
|
||||
}
|
||||
@@ -170,16 +163,14 @@ class Room {
|
||||
return this.players.map(p => ({
|
||||
name: p.name,
|
||||
teamIndex: p.teamIndex,
|
||||
score: p.score,
|
||||
gauges: { ...p.gauges },
|
||||
score: ATTRS.reduce((sum, a) => sum + p.gauges[a], 0),
|
||||
handSize: p.hand.length
|
||||
}))
|
||||
}
|
||||
|
||||
/** État envoyé à UN joueur spécifique (inclut sa main et sa phase personnelle) */
|
||||
getStateFor(socketId) {
|
||||
const player = this.players.find(p => p.socketId === socketId)
|
||||
// Si ce joueur a déjà joué ce tour → sa phase perso est 'waiting'
|
||||
// Sinon il est encore en 'selecting'
|
||||
const hasPlayed = this.pendingPlays.has(socketId)
|
||||
const personalPhase = this.phase === 'selecting' && hasPlayed ? 'waiting' : this.phase
|
||||
|
||||
|
||||
+2
-2
@@ -81,10 +81,10 @@ io.on('connection', (socket) => {
|
||||
const roundResult = room.resolveRound()
|
||||
console.log(`[${roomId}] Tour ${room.currentTurn} résolu`)
|
||||
|
||||
// Révéler les cartes jouées et les résultats
|
||||
// Révéler les cartes jouées et les gains par attribut
|
||||
io.to(roomId).emit('round-result', {
|
||||
playedCards: roundResult.playedCards,
|
||||
attrResults: roundResult.attrResults,
|
||||
gains: roundResult.gains,
|
||||
message: roundResult.message
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user