feat: 12 character cards with 4 color-coded attributes (food/war/health/diplo)
This commit is contained in:
1 parent
1db221e468
commit
38814668e7
7 files changed
+2190
-317
No files matched your search
@@ -1,99 +1,160 @@
|
||||
import Phaser from 'phaser'
|
||||
import { CardObject } from '../objects/CardObject'
|
||||
|
||||
// Couleurs équipes (synchronisé avec le serveur)
|
||||
const TEAM_COLORS = {
|
||||
0: 0xcc2200,
|
||||
1: 0x1155cc,
|
||||
2: 0x1a7a2e,
|
||||
3: 0xb8860b
|
||||
}
|
||||
|
||||
// Couleurs attributs
|
||||
const ATTR_COLORS = {
|
||||
food: 0xf0c030,
|
||||
war: 0xdd2222,
|
||||
health: 0x22aa44,
|
||||
diplo: 0x3399ee
|
||||
}
|
||||
|
||||
const ATTR_LABELS = {
|
||||
food: 'Nourriture',
|
||||
war: 'Guerre',
|
||||
health: 'Santé',
|
||||
diplo: 'Diplomatie'
|
||||
}
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super('GameScene')
|
||||
this.hand = []
|
||||
this.handCards = []
|
||||
this.selectedCard = null
|
||||
this.playedCards = []
|
||||
this.resolveObjects = []
|
||||
}
|
||||
|
||||
create() {
|
||||
this.socket = this.registry.get('socket')
|
||||
this.store = this.registry.get('store')
|
||||
this.roomId = this.registry.get('roomId')
|
||||
this.socket = this.registry.get('socket')
|
||||
this.store = this.registry.get('store')
|
||||
this.roomId = this.registry.get('roomId')
|
||||
|
||||
this.createTable()
|
||||
this.createHand()
|
||||
this.createPlayButton()
|
||||
this.setupSocketListeners()
|
||||
this._buildTable()
|
||||
this._buildPlayButton()
|
||||
this._setupSocketListeners()
|
||||
|
||||
// Demander l'état initial
|
||||
this.socket.emit('request-state', { roomId: this.roomId })
|
||||
}
|
||||
|
||||
createTable() {
|
||||
// ─── Construction du plateau ─────────────────────────────────────────────────
|
||||
|
||||
_buildTable() {
|
||||
const { width, height } = this.scale
|
||||
|
||||
// Zone centrale de jeu
|
||||
this.add.rectangle(width / 2, height / 2, width * 0.6, height * 0.4, 0x1a0800, 0.8)
|
||||
.setStrokeStyle(1, 0xd4a73240)
|
||||
// Fond parchemin
|
||||
this.add.rectangle(width / 2, height / 2, width, height, 0x0d0500)
|
||||
|
||||
// Label tour
|
||||
this.turnText = this.add.text(width / 2, 30, 'Tour 1/10', {
|
||||
fontSize: '18px',
|
||||
color: '#d4a732',
|
||||
fontFamily: 'Georgia'
|
||||
// Zone de jeu centrale
|
||||
this.add.rectangle(width / 2, height / 2 - 60, width * 0.7, height * 0.38, 0x1a0800, 0.7)
|
||||
.setStrokeStyle(1, 0xd4a73230)
|
||||
|
||||
// Titre du tour
|
||||
this.turnText = this.add.text(width / 2, 18, 'Tour 1 / 12', {
|
||||
fontSize: '16px',
|
||||
fontFamily: 'Georgia',
|
||||
color: '#d4a732'
|
||||
}).setOrigin(0.5)
|
||||
|
||||
// Zone cartes jouées (4 emplacements)
|
||||
this.playZones = []
|
||||
const zoneY = height / 2
|
||||
const startX = width / 2 - 180
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const x = startX + i * 120
|
||||
const zone = this.add.rectangle(x, zoneY, 110, 170, 0x000000, 0)
|
||||
.setStrokeStyle(1, 0xd4a73250)
|
||||
this.playZones.push(zone)
|
||||
// Phase courante
|
||||
this.phaseText = this.add.text(width / 2, 40, 'Sélectionnez une carte', {
|
||||
fontSize: '13px',
|
||||
fontFamily: 'Georgia',
|
||||
color: '#a08060'
|
||||
}).setOrigin(0.5)
|
||||
|
||||
this.add.text(x, zoneY + 100, `J${i + 1}`, {
|
||||
fontSize: '12px',
|
||||
color: '#a08060',
|
||||
fontFamily: 'Georgia'
|
||||
// 4 emplacements de cartes jouées (zone centrale)
|
||||
this.playSlots = []
|
||||
const slotY = height / 2 - 60
|
||||
const totalW = 4 * 140
|
||||
const startX = width / 2 - totalW / 2 + 70
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const x = startX + i * 140
|
||||
const slot = this.add.graphics()
|
||||
slot.lineStyle(1, 0xd4a73240)
|
||||
slot.strokeRoundedRect(-65, -95, 130, 190, 8)
|
||||
slot.x = x
|
||||
slot.y = slotY
|
||||
this.playSlots.push({ x, y: slotY, graphics: slot })
|
||||
|
||||
// Label joueur
|
||||
this.add.text(x, slotY + 108, `J${i + 1}`, {
|
||||
fontSize: '11px',
|
||||
fontFamily: 'Georgia',
|
||||
color: '#4a3020'
|
||||
}).setOrigin(0.5)
|
||||
}
|
||||
|
||||
// Légende attributs (coin en bas à droite)
|
||||
this._buildAttrLegend(width - 10, height - 10)
|
||||
}
|
||||
|
||||
createHand() {
|
||||
const { width, height } = this.scale
|
||||
const handY = height - 110
|
||||
_buildAttrLegend(x, y) {
|
||||
const entries = [
|
||||
{ attr: 'food', label: '🟡 Nourriture — haut gauche' },
|
||||
{ attr: 'war', label: '🔴 Guerre — haut droite' },
|
||||
{ attr: 'health', label: '🟢 Santé — bas gauche' },
|
||||
{ attr: 'diplo', label: '🔵 Diplomatie — bas droite' }
|
||||
]
|
||||
|
||||
// Vider l'ancienne main
|
||||
this.hand.forEach(c => c.destroy())
|
||||
this.hand = []
|
||||
|
||||
const cards = this.store.hand
|
||||
const totalWidth = cards.length * 130
|
||||
const startX = width / 2 - totalWidth / 2 + 60
|
||||
|
||||
cards.forEach((cardData, i) => {
|
||||
const card = new CardObject(this, startX + i * 130, handY, cardData)
|
||||
card.on('select', this.onCardSelected, this)
|
||||
this.hand.push(card)
|
||||
entries.forEach((e, i) => {
|
||||
this.add.text(x, y - i * 16, e.label, {
|
||||
fontSize: '10px',
|
||||
fontFamily: 'Georgia',
|
||||
color: '#6a5040'
|
||||
}).setOrigin(1, 1)
|
||||
})
|
||||
}
|
||||
|
||||
createPlayButton() {
|
||||
_buildPlayButton() {
|
||||
const { width, height } = this.scale
|
||||
|
||||
this.playBtn = this.add.text(width / 2, height - 30, '[ JOUER LA CARTE ]', {
|
||||
fontSize: '16px',
|
||||
this.playBtn = this.add.text(width / 2, height - 18, '[ JOUER LA CARTE ]', {
|
||||
fontSize: '15px',
|
||||
fontFamily: 'Georgia',
|
||||
color: '#1a0a00',
|
||||
backgroundColor: '#d4a732',
|
||||
padding: { x: 20, y: 8 },
|
||||
fontFamily: 'Georgia'
|
||||
padding: { x: 20, y: 8 }
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setAlpha(0.35)
|
||||
.setInteractive({ cursor: 'pointer' })
|
||||
.setAlpha(0.4)
|
||||
.on('pointerdown', this.playSelectedCard, this)
|
||||
.on('pointerover', () => { if (this.selectedCard) this.playBtn.setAlpha(1) })
|
||||
.on('pointerout', () => { if (!this.selectedCard) this.playBtn.setAlpha(0.4) })
|
||||
.on('pointerdown', this._playSelectedCard, this)
|
||||
}
|
||||
|
||||
onCardSelected(card) {
|
||||
// Désélectionner la précédente
|
||||
// ─── Main du joueur ──────────────────────────────────────────────────────────
|
||||
|
||||
_buildHand(cards) {
|
||||
const { width, height } = this.scale
|
||||
|
||||
// Vider l'ancienne main
|
||||
this.handCards.forEach(c => c.destroy())
|
||||
this.handCards = []
|
||||
|
||||
if (!cards || cards.length === 0) return
|
||||
|
||||
const handY = height - 120
|
||||
const spacing = Math.min(150, (width * 0.85) / cards.length)
|
||||
const totalW = spacing * (cards.length - 1)
|
||||
const startX = width / 2 - totalW / 2
|
||||
|
||||
cards.forEach((cardData, i) => {
|
||||
const card = new CardObject(this, startX + i * spacing, handY, cardData)
|
||||
card.on('select', this._onCardSelected, this)
|
||||
this.handCards.push(card)
|
||||
})
|
||||
}
|
||||
|
||||
_onCardSelected(card) {
|
||||
// Désélectionner l'ancienne
|
||||
if (this.selectedCard && this.selectedCard !== card) {
|
||||
this.selectedCard.deselect()
|
||||
}
|
||||
@@ -102,7 +163,7 @@ export class GameScene extends Phaser.Scene {
|
||||
this.playBtn.setAlpha(1)
|
||||
}
|
||||
|
||||
playSelectedCard() {
|
||||
_playSelectedCard() {
|
||||
if (!this.selectedCard || this.store.phase !== 'selecting') return
|
||||
|
||||
this.socket.emit('play-card', {
|
||||
@@ -110,55 +171,146 @@ export class GameScene extends Phaser.Scene {
|
||||
cardId: this.selectedCard.cardData.id
|
||||
})
|
||||
|
||||
this.selectedCard.setAlpha(0.5)
|
||||
this.selectedCard.setAlpha(0.4)
|
||||
this.selectedCard = null
|
||||
this.store.clearSelection()
|
||||
this.playBtn.setAlpha(0.4)
|
||||
this.playBtn.setAlpha(0.35)
|
||||
this.phaseText.setText('En attente des autres joueurs...')
|
||||
}
|
||||
|
||||
setupSocketListeners() {
|
||||
// ─── Résolution visuelle ─────────────────────────────────────────────────────
|
||||
|
||||
_showPlayedCards(playedCards) {
|
||||
// Nettoyer
|
||||
this.resolveObjects.forEach(o => o.destroy())
|
||||
this.resolveObjects = []
|
||||
|
||||
playedCards.forEach((entry, i) => {
|
||||
const slot = this.playSlots[i]
|
||||
if (!slot) return
|
||||
|
||||
const teamColor = TEAM_COLORS[entry.teamIndex] ?? 0x888888
|
||||
|
||||
// Mini carte de résolution
|
||||
const g = this.add.graphics()
|
||||
g.fillStyle(this._darken(teamColor, 0.3))
|
||||
g.fillRoundedRect(-55, -85, 110, 170, 6)
|
||||
g.lineStyle(2, teamColor)
|
||||
g.strokeRoundedRect(-55, -85, 110, 170, 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
|
||||
}).setOrigin(0.5)
|
||||
this.resolveObjects.push(v)
|
||||
|
||||
const l = this.add.text(cx, cy + 10, label, {
|
||||
fontSize: '7px', fontFamily: 'Arial', color: '#ffffff'
|
||||
}).setOrigin(0.5)
|
||||
this.resolveObjects.push(l)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_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() {
|
||||
this.socket.on('game-state', (state) => {
|
||||
this.store.updateState(state)
|
||||
this.turnText.setText(`Tour ${state.currentTurn}/${state.maxTurns}`)
|
||||
this.turnText.setText(`Tour ${state.currentTurn} / ${state.maxTurns}`)
|
||||
|
||||
if (state.phase === 'selecting') {
|
||||
this.createHand()
|
||||
this.resolveObjects.forEach(o => o.destroy())
|
||||
this.resolveObjects = []
|
||||
this.phaseText.setText('Sélectionnez une carte')
|
||||
this._buildHand(state.hand)
|
||||
}
|
||||
|
||||
if (state.phase === 'resolve') {
|
||||
this.showPlayedCards(state.playedCards)
|
||||
if (state.phase === 'waiting') {
|
||||
this.phaseText.setText('En attente des autres joueurs...')
|
||||
}
|
||||
})
|
||||
|
||||
this.socket.on('round-result', (result) => {
|
||||
this.showRoundResult(result)
|
||||
this.socket.on('round-result', ({ playedCards, attrResults }) => {
|
||||
this.phaseText.setText('Résolution du tour...')
|
||||
this._showPlayedCards(playedCards)
|
||||
this._showAttrResults(attrResults)
|
||||
})
|
||||
}
|
||||
|
||||
showPlayedCards(played) {
|
||||
played.forEach((entry, i) => {
|
||||
if (!this.playZones[i]) return
|
||||
const zone = this.playZones[i]
|
||||
this.add.text(zone.x, zone.y, entry.cardName, {
|
||||
fontSize: '13px',
|
||||
color: '#f5e6c8',
|
||||
fontFamily: 'Georgia',
|
||||
wordWrap: { width: 100 }
|
||||
}).setOrigin(0.5).setDepth(10)
|
||||
})
|
||||
}
|
||||
// ─── Utilitaires ─────────────────────────────────────────────────────────────
|
||||
|
||||
showRoundResult(result) {
|
||||
const { width, height } = this.scale
|
||||
const txt = this.add.text(width / 2, height / 2 - 100, result.message, {
|
||||
fontSize: '20px',
|
||||
color: '#d4a732',
|
||||
fontFamily: 'Georgia',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 4
|
||||
}).setOrigin(0.5).setDepth(20)
|
||||
|
||||
this.time.delayedCall(2500, () => txt.destroy())
|
||||
_darken(hex, factor) {
|
||||
const r = Math.floor(((hex >> 16) & 0xff) * factor)
|
||||
const g = Math.floor(((hex >> 8) & 0xff) * factor)
|
||||
const b = Math.floor(( hex & 0xff) * factor)
|
||||
return (r << 16) | (g << 8) | b
|
||||
}
|
||||
|
||||
update() {}
|
||||
|
||||
Reference in new issue
Block a user