feat: gauge scoring system (subtract left neighbor per attribute)

This commit is contained in:
kevin.lhereec committed 2026-08-17 11:54:23 +02:00
1 parent 25ab76c4ac
commit 91a1a9f4f0
5 files changed
+297 -203

No files matched your search

+70 -85
View File
@@ -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
View File
@@ -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
View File
@@ -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>