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
Generated
+10
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@@ -1,106 +1,198 @@
|
||||
import Phaser from 'phaser'
|
||||
|
||||
// Couleurs des 4 attributs (identiques au serveur)
|
||||
const ATTR = {
|
||||
food: { color: 0xf0c030, label: 'NOU' },
|
||||
war: { color: 0xdd2222, label: 'GUE' },
|
||||
health: { color: 0x22aa44, label: 'SAN' },
|
||||
diplo: { color: 0x3399ee, label: 'DIP' }
|
||||
}
|
||||
|
||||
const CARD_W = 130
|
||||
const CARD_H = 190
|
||||
const CORNER = 28 // taille du badge de coin
|
||||
const RADIUS = 10
|
||||
|
||||
export class CardObject extends Phaser.GameObjects.Container {
|
||||
/**
|
||||
* @param {Phaser.Scene} scene
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {object} cardData - { id, name, food, war, health, diplo, teamColor: { hex } }
|
||||
*/
|
||||
constructor(scene, x, y, cardData) {
|
||||
super(scene, x, y)
|
||||
this.cardData = cardData
|
||||
this._selected = false
|
||||
this._baseY = y
|
||||
|
||||
this.build()
|
||||
this._build()
|
||||
scene.add.existing(this)
|
||||
}
|
||||
|
||||
build() {
|
||||
// Fond de carte
|
||||
this.bg = this.scene.add.image(0, 0, 'card-front')
|
||||
this.add(this.bg)
|
||||
_build() {
|
||||
const teamHex = this.cardData.teamColor?.hex ?? 0x888888
|
||||
|
||||
// Nom de la carte
|
||||
this.nameText = this.scene.add.text(0, -60, this.cardData.name, {
|
||||
fontSize: '12px',
|
||||
color: '#f5e6c8',
|
||||
fontFamily: 'Georgia',
|
||||
wordWrap: { width: 100 },
|
||||
// --- Fond de carte avec couleur d'équipe ---
|
||||
const bg = this.scene.add.graphics()
|
||||
// Ombre portée
|
||||
bg.fillStyle(0x000000, 0.4)
|
||||
bg.fillRoundedRect(4, 4, CARD_W, CARD_H, RADIUS)
|
||||
// Corps principal (couleur équipe assombrie)
|
||||
bg.fillStyle(this._darken(teamHex, 0.3))
|
||||
bg.fillRoundedRect(0, 0, CARD_W, CARD_H, RADIUS)
|
||||
// Bordure couleur équipe
|
||||
bg.lineStyle(2, teamHex, 1)
|
||||
bg.strokeRoundedRect(0, 0, CARD_W, CARD_H, RADIUS)
|
||||
// Zone portrait (rectangle foncé central)
|
||||
bg.fillStyle(0x000000, 0.35)
|
||||
bg.fillRoundedRect(8, 28, CARD_W - 16, CARD_H - 80, 6)
|
||||
bg.x = -CARD_W / 2
|
||||
bg.y = -CARD_H / 2
|
||||
this.add(bg)
|
||||
|
||||
// --- Nom du personnage (haut, centré) ---
|
||||
const nameText = this.scene.add.text(0, -CARD_H / 2 + 9, this.cardData.name, {
|
||||
fontSize: '13px',
|
||||
fontFamily: 'Georgia, serif',
|
||||
color: '#ffffff',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 3,
|
||||
fontStyle: 'bold'
|
||||
}).setOrigin(0.5, 0)
|
||||
this.add(nameText)
|
||||
|
||||
// --- Initiale du personnage (zone portrait) ---
|
||||
const initial = this.scene.add.text(0, -CARD_H / 2 + 75, this.cardData.name[0], {
|
||||
fontSize: '52px',
|
||||
fontFamily: 'Georgia, serif',
|
||||
color: '#ffffff',
|
||||
alpha: 0.15
|
||||
}).setOrigin(0.5, 0.5)
|
||||
this.add(initial)
|
||||
|
||||
// --- Description courte (bas de carte) ---
|
||||
const desc = this.scene.add.text(0, CARD_H / 2 - 22, this._shortDesc(), {
|
||||
fontSize: '8px',
|
||||
fontFamily: 'Georgia, serif',
|
||||
color: '#cccccc',
|
||||
wordWrap: { width: CARD_W - 16 },
|
||||
align: 'center'
|
||||
}).setOrigin(0.5)
|
||||
this.add(this.nameText)
|
||||
}).setOrigin(0.5, 1)
|
||||
this.add(desc)
|
||||
|
||||
// Valeur/effet
|
||||
this.valueText = this.scene.add.text(0, 10, `+${this.cardData.points} pts`, {
|
||||
fontSize: '18px',
|
||||
color: '#d4a732',
|
||||
fontFamily: 'Georgia'
|
||||
}).setOrigin(0.5)
|
||||
this.add(this.valueText)
|
||||
// --- 4 badges d'attributs dans les coins ---
|
||||
this._addCornerBadge('food', -CARD_W / 2 + 2, -CARD_H / 2 + 2, 'topLeft')
|
||||
this._addCornerBadge('war', CARD_W / 2 - 2, -CARD_H / 2 + 2, 'topRight')
|
||||
this._addCornerBadge('health', -CARD_W / 2 + 2, CARD_H / 2 - 2, 'bottomLeft')
|
||||
this._addCornerBadge('diplo', CARD_W / 2 - 2, CARD_H / 2 - 2, 'bottomRight')
|
||||
|
||||
// Type de carte
|
||||
this.typeText = this.scene.add.text(0, 50, this.cardData.type, {
|
||||
fontSize: '10px',
|
||||
color: '#a08060',
|
||||
fontFamily: 'Georgia'
|
||||
}).setOrigin(0.5)
|
||||
this.add(this.typeText)
|
||||
|
||||
// Interactivité
|
||||
this.setSize(120, 180)
|
||||
// --- Interactivité ---
|
||||
this.setSize(CARD_W, CARD_H)
|
||||
this.setInteractive({ cursor: 'pointer' })
|
||||
|
||||
this.on('pointerover', this.onHover, this)
|
||||
this.on('pointerout', this.onOut, this)
|
||||
this.on('pointerdown', this.onDown, this)
|
||||
this.on('pointerover', this._onHover, this)
|
||||
this.on('pointerout', this._onOut, this)
|
||||
this.on('pointerdown', this._onDown, this)
|
||||
}
|
||||
|
||||
onHover() {
|
||||
if (!this._selected) {
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
y: this.y - 15,
|
||||
duration: 150,
|
||||
ease: 'Power2'
|
||||
})
|
||||
/**
|
||||
* Badge carré coloré dans un coin, avec la valeur de l'attribut
|
||||
* @param {'food'|'war'|'health'|'diplo'} attr
|
||||
* @param {number} bx position x relative au centre du container (coin du badge)
|
||||
* @param {number} by position y relative au centre du container (coin du badge)
|
||||
* @param {'topLeft'|'topRight'|'bottomLeft'|'bottomRight'} corner
|
||||
*/
|
||||
_addCornerBadge(attr, bx, by, corner) {
|
||||
const { color, label } = ATTR[attr]
|
||||
const value = this.cardData[attr]
|
||||
const S = CORNER
|
||||
|
||||
// Calculer l'origine du rectangle selon le coin
|
||||
let rx = bx, ry = by
|
||||
if (corner === 'topRight' || corner === 'bottomRight') rx = bx - S
|
||||
if (corner === 'bottomLeft' || corner === 'bottomRight') ry = by - S
|
||||
|
||||
const g = this.scene.add.graphics()
|
||||
g.fillStyle(color, 0.9)
|
||||
// Coins arrondis uniquement vers l'intérieur de la carte
|
||||
const cornerRadii = {
|
||||
topLeft: { tl: RADIUS, tr: 2, bl: 2, br: 4 },
|
||||
topRight: { tl: 2, tr: RADIUS, bl: 4, br: 2 },
|
||||
bottomLeft: { tl: 2, tr: 4, bl: RADIUS, br: 2 },
|
||||
bottomRight: { tl: 4, tr: 2, bl: 2, br: RADIUS }
|
||||
}
|
||||
g.fillRoundedRect(rx, ry, S, S, cornerRadii[corner])
|
||||
this.add(g)
|
||||
|
||||
// Valeur (grand, centré dans le badge)
|
||||
const cx = rx + S / 2
|
||||
const cy = ry + S / 2 - 3
|
||||
const valText = this.scene.add.text(cx, cy, String(value), {
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Georgia, serif',
|
||||
fontStyle: 'bold',
|
||||
color: '#ffffff',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 2
|
||||
}).setOrigin(0.5, 0.5)
|
||||
this.add(valText)
|
||||
|
||||
// Label abrégé (petit, en bas du badge)
|
||||
const lblText = this.scene.add.text(cx, cy + 8, label, {
|
||||
fontSize: '6px',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
color: '#ffffff',
|
||||
alpha: 0.85
|
||||
}).setOrigin(0.5, 0.5)
|
||||
this.add(lblText)
|
||||
}
|
||||
|
||||
onOut() {
|
||||
if (!this._selected) {
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
y: this.y + 15,
|
||||
duration: 150,
|
||||
ease: 'Power2'
|
||||
})
|
||||
}
|
||||
// --- Interactions ---
|
||||
|
||||
_onHover() {
|
||||
if (this._selected) return
|
||||
this.scene.tweens.add({ targets: this, y: this._baseY - 18, duration: 120, ease: 'Power2' })
|
||||
this.setDepth(10)
|
||||
}
|
||||
|
||||
onDown() {
|
||||
this._selected = !this._selected
|
||||
_onOut() {
|
||||
if (this._selected) return
|
||||
this.scene.tweens.add({ targets: this, y: this._baseY, duration: 120, ease: 'Power2' })
|
||||
this.setDepth(0)
|
||||
}
|
||||
|
||||
_onDown() {
|
||||
if (this._selected) {
|
||||
this.deselect()
|
||||
} else {
|
||||
this.select()
|
||||
this.emit('select', this)
|
||||
} else {
|
||||
this.deselect()
|
||||
}
|
||||
}
|
||||
|
||||
select() {
|
||||
this._selected = true
|
||||
this.bg.setTint(0xd4a732)
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
y: this.y - 20,
|
||||
duration: 150,
|
||||
ease: 'Power2'
|
||||
})
|
||||
this.setDepth(20)
|
||||
this.scene.tweens.add({ targets: this, y: this._baseY - 28, duration: 150, ease: 'Back.Out' })
|
||||
}
|
||||
|
||||
deselect() {
|
||||
this._selected = false
|
||||
this.bg.clearTint()
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
y: this.y + 20,
|
||||
duration: 150,
|
||||
ease: 'Power2'
|
||||
})
|
||||
this.setDepth(0)
|
||||
this.scene.tweens.add({ targets: this, y: this._baseY, duration: 150, ease: 'Power2' })
|
||||
}
|
||||
|
||||
// --- Utilitaires ---
|
||||
|
||||
_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
|
||||
}
|
||||
|
||||
_shortDesc() {
|
||||
const d = this.cardData.description || ''
|
||||
return d.length > 60 ? d.substring(0, 57) + '…' : d
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
|
||||
Generated
+1465
File diff suppressed because it is too large.
Load diff
+109
-99
@@ -1,22 +1,32 @@
|
||||
const { dealHands, createDeck } = require('./cards')
|
||||
const { dealHands } = require('./cards')
|
||||
|
||||
const MAX_PLAYERS = 4
|
||||
const MAX_TURNS = 10
|
||||
const HAND_SIZE = 5
|
||||
const MAX_TURNS = 12 // 12 tours (1 par personnage potentiellement)
|
||||
const HAND_SIZE = 4 // 4 cartes en main au départ
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
class Room {
|
||||
constructor(id) {
|
||||
this.id = id
|
||||
this.players = [] // { socketId, name, score, hand, protected }
|
||||
this.phase = 'lobby' // lobby | selecting | waiting | resolve | end
|
||||
this.id = id
|
||||
this.players = [] // { socketId, name, teamIndex, score, hand }
|
||||
this.phase = 'lobby' // lobby | selecting | waiting | resolve | end
|
||||
this.currentTurn = 0
|
||||
this.maxTurns = MAX_TURNS
|
||||
this.maxTurns = MAX_TURNS
|
||||
this.pendingPlays = new Map() // socketId -> card
|
||||
this.deck = []
|
||||
}
|
||||
|
||||
get playerCount() { return this.players.length }
|
||||
|
||||
// ─── Gestion des joueurs ────────────────────────────────────────────────────
|
||||
|
||||
addPlayer(socketId, name) {
|
||||
if (this.players.length >= MAX_PLAYERS) return false
|
||||
if (this.players.find(p => p.socketId === socketId)) return true
|
||||
@@ -24,9 +34,9 @@ class Room {
|
||||
this.players.push({
|
||||
socketId,
|
||||
name,
|
||||
teamIndex: this.players.length, // 0=Écarlate 1=Azur 2=Émeraude 3=Or
|
||||
score: 0,
|
||||
hand: [],
|
||||
protected: false
|
||||
hand: []
|
||||
})
|
||||
return true
|
||||
}
|
||||
@@ -35,29 +45,38 @@ class Room {
|
||||
this.players = this.players.filter(p => p.socketId !== socketId)
|
||||
}
|
||||
|
||||
// ─── Démarrage ──────────────────────────────────────────────────────────────
|
||||
|
||||
startGame() {
|
||||
const { hands, remaining } = dealHands(this.players.length, HAND_SIZE)
|
||||
this.deck = remaining
|
||||
const hands = dealHands(this.players.length, HAND_SIZE)
|
||||
this.players.forEach((p, i) => { p.hand = hands[i] })
|
||||
this.currentTurn = 1
|
||||
this.phase = 'selecting'
|
||||
this.pendingPlays.clear()
|
||||
}
|
||||
|
||||
// ─── Phase de jeu ───────────────────────────────────────────────────────────
|
||||
|
||||
playCard(socketId, cardId) {
|
||||
if (this.phase !== 'selecting') return { ok: false, error: 'Ce n\'est pas la phase de jeu' }
|
||||
if (this.pendingPlays.has(socketId)) return { ok: false, error: 'Tu as déjà joué ce tour' }
|
||||
if (this.phase !== 'selecting')
|
||||
return { ok: false, error: 'Ce n\'est pas la phase de sélection' }
|
||||
if (this.pendingPlays.has(socketId))
|
||||
return { ok: false, error: 'Tu as déjà joué une carte ce tour' }
|
||||
|
||||
const player = this.players.find(p => p.socketId === socketId)
|
||||
if (!player) return { ok: false, error: 'Joueur introuvable' }
|
||||
|
||||
const cardIndex = player.hand.findIndex(c => c.id === cardId)
|
||||
if (cardIndex === -1) return { ok: false, error: 'Carte non trouvée dans ta main' }
|
||||
if (cardIndex === -1) return { ok: false, error: 'Carte absente de ta main' }
|
||||
|
||||
const card = player.hand.splice(cardIndex, 1)[0]
|
||||
const [card] = player.hand.splice(cardIndex, 1)
|
||||
this.pendingPlays.set(socketId, { player, card })
|
||||
|
||||
this.phase = 'waiting'
|
||||
// Si tous ont joué → passer en "waiting" visible côté client
|
||||
if (this.pendingPlays.size === this.players.length) {
|
||||
this.phase = 'waiting'
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
@@ -65,119 +84,110 @@ class Room {
|
||||
return this.pendingPlays.size === this.players.length
|
||||
}
|
||||
|
||||
// ─── Résolution du tour ─────────────────────────────────────────────────────
|
||||
|
||||
resolveRound() {
|
||||
const plays = [...this.pendingPlays.values()]
|
||||
const playedCards = plays.map(p => ({ name: p.player.name, cardName: p.card.name }))
|
||||
|
||||
// Réinitialiser protection
|
||||
this.players.forEach(p => { p.protected = false })
|
||||
// Cartes jouées (pour 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
|
||||
}))
|
||||
|
||||
// 1. Appliquer les effets de protection (Enchanteur)
|
||||
plays.forEach(({ player, card }) => {
|
||||
if (card.effect === 'protect') {
|
||||
player.protected = true
|
||||
// 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 }
|
||||
})
|
||||
|
||||
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) }
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Calculer les points de base et effets
|
||||
const messages = []
|
||||
plays.forEach(({ player, card }) => {
|
||||
// Points de base
|
||||
player.score += card.points
|
||||
|
||||
if (!card.effect) return
|
||||
|
||||
const [effectType, valueStr] = card.effect.split(':')
|
||||
const value = parseInt(valueStr) || 0
|
||||
|
||||
switch (effectType) {
|
||||
case 'steal': {
|
||||
// Voler des points au joueur le plus riche non protégé
|
||||
const targets = this.players
|
||||
.filter(p => p.socketId !== player.socketId && !p.protected && p.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
|
||||
if (targets.length > 0) {
|
||||
const stolen = Math.min(value, targets[0].score)
|
||||
targets[0].score -= stolen
|
||||
player.score += stolen
|
||||
messages.push(`${player.name} vole ${stolen} pts à ${targets[0].name}`)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'bonus': {
|
||||
// Bonus si le joueur a le plus de points de Royaume ce tour
|
||||
const myRoyaume = plays
|
||||
.filter(p => p.player.socketId === player.socketId && p.card.type === 'Royaume')
|
||||
.reduce((sum, p) => sum + p.card.points, 0)
|
||||
if (myRoyaume > 0) {
|
||||
player.score += value
|
||||
messages.push(`${player.name} gagne +${value} pts bonus (Commerce)`)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'double': {
|
||||
// Double les points gagnés CE tour (uniquement la carte jouée)
|
||||
player.score += card.points
|
||||
messages.push(`${player.name} double ses gains (Alchimiste)`)
|
||||
break
|
||||
}
|
||||
|
||||
case 'draw': {
|
||||
// Piocher une carte supplémentaire
|
||||
if (this.deck.length > 0) {
|
||||
player.hand.push(this.deck.pop())
|
||||
messages.push(`${player.name} pioche une carte bonus`)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
// 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`
|
||||
})
|
||||
|
||||
const message = messages.length > 0 ? messages.join(' | ') : 'Tour résolu !'
|
||||
|
||||
this.pendingPlays.clear()
|
||||
this.phase = 'resolve'
|
||||
|
||||
return { playedCards, message }
|
||||
return {
|
||||
playedCards,
|
||||
attrResults,
|
||||
pointsGained,
|
||||
message: lines.join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tour suivant ────────────────────────────────────────────────────────────
|
||||
|
||||
nextTurn() {
|
||||
this.currentTurn++
|
||||
this.phase = 'selecting'
|
||||
|
||||
// Distribuer une nouvelle carte si la main est vide
|
||||
this.players.forEach(p => {
|
||||
if (p.hand.length === 0 && this.deck.length >= HAND_SIZE) {
|
||||
p.hand = this.deck.splice(0, HAND_SIZE)
|
||||
} else if (p.hand.length < 2 && this.deck.length > 0) {
|
||||
p.hand.push(this.deck.pop())
|
||||
}
|
||||
})
|
||||
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() {
|
||||
return this.currentTurn >= this.maxTurns
|
||||
// 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
|
||||
}
|
||||
|
||||
// ─── Données publiques ───────────────────────────────────────────────────────
|
||||
|
||||
getPlayersInfo() {
|
||||
return this.players.map(p => ({
|
||||
name: p.name,
|
||||
score: p.score,
|
||||
handSize: p.hand.length
|
||||
name: p.name,
|
||||
teamIndex: p.teamIndex,
|
||||
score: p.score,
|
||||
handSize: p.hand.length
|
||||
}))
|
||||
}
|
||||
|
||||
/** État envoyé à UN joueur spécifique (inclut sa main) */
|
||||
getStateFor(socketId) {
|
||||
const player = this.players.find(p => p.socketId === socketId)
|
||||
return {
|
||||
players: this.getPlayersInfo(),
|
||||
players: this.getPlayersInfo(),
|
||||
currentTurn: this.currentTurn,
|
||||
maxTurns: this.maxTurns,
|
||||
phase: this.phase,
|
||||
hand: player ? player.hand : []
|
||||
maxTurns: this.maxTurns,
|
||||
phase: this.phase,
|
||||
hand: player ? player.hand : [],
|
||||
myTeamIndex: player ? player.teamIndex : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+160
-34
@@ -1,31 +1,161 @@
|
||||
// Deck de base de Regnum
|
||||
// Types: Royaume (points fixes), Armée (attaque), Commerce (multiplicateur), Magie (effets spéciaux)
|
||||
/**
|
||||
* Regnum - Définition des 12 personnages
|
||||
*
|
||||
* Chaque carte possède 4 attributs (valeurs de 1 à 6) :
|
||||
* food (🟡 Nourriture) - coin haut gauche - jaune
|
||||
* war (🔴 Guerre) - coin haut droite - rouge
|
||||
* health(🟢 Santé) - coin bas gauche - vert
|
||||
* diplo (🔵 Diplomatie) - coin bas droite - bleu
|
||||
*
|
||||
* Toutes les équipes ont exactement le même deck de 12 cartes,
|
||||
* seule la couleur d'équipe change visuellement.
|
||||
*/
|
||||
|
||||
const BASE_DECK = [
|
||||
// === ROYAUME ===
|
||||
{ id: 'r1', name: 'Village', type: 'Royaume', points: 2, effect: null },
|
||||
{ id: 'r2', name: 'Château', type: 'Royaume', points: 5, effect: null },
|
||||
{ id: 'r3', name: 'Cathédrale', type: 'Royaume', points: 4, effect: null },
|
||||
{ id: 'r4', name: 'Forteresse', type: 'Royaume', points: 3, effect: null },
|
||||
{ id: 'r5', name: 'Palais Royal', type: 'Royaume', points: 7, effect: null },
|
||||
|
||||
// === ARMÉE ===
|
||||
{ id: 'a1', name: 'Chevaliers', type: 'Armée', points: 3, effect: 'steal:1' },
|
||||
{ id: 'a2', name: 'Archers', type: 'Armée', points: 2, effect: 'steal:1' },
|
||||
{ id: 'a3', name: 'Catapulte', type: 'Armée', points: 1, effect: 'steal:2' },
|
||||
{ id: 'a4', name: 'Légion', type: 'Armée', points: 4, effect: 'steal:2' },
|
||||
|
||||
// === COMMERCE ===
|
||||
{ id: 'c1', name: 'Marché', type: 'Commerce', points: 2, effect: 'bonus:1' },
|
||||
{ id: 'c2', name: 'Port', type: 'Commerce', points: 3, effect: 'bonus:2' },
|
||||
{ id: 'c3', name: 'Route de soie', type: 'Commerce', points: 5, effect: 'bonus:1' },
|
||||
|
||||
// === MAGIE ===
|
||||
{ id: 'm1', name: 'Oracle', type: 'Magie', points: 1, effect: 'draw:1' },
|
||||
{ id: 'm2', name: 'Enchanteur', type: 'Magie', points: 2, effect: 'protect' },
|
||||
{ id: 'm3', name: 'Alchimiste', type: 'Magie', points: 3, effect: 'double' },
|
||||
const CHARACTERS = [
|
||||
{
|
||||
id: 'queen',
|
||||
name: 'Reine',
|
||||
food: 2,
|
||||
war: 2,
|
||||
health: 3,
|
||||
diplo: 5,
|
||||
description: 'Maîtresse de la diplomatie, elle tisse des alliances avec dextérité.'
|
||||
},
|
||||
{
|
||||
id: 'prince',
|
||||
name: 'Prince',
|
||||
food: 2,
|
||||
war: 4,
|
||||
health: 2,
|
||||
diplo: 4,
|
||||
description: 'Héritier du trône, équilibré entre le glaive et la parole.'
|
||||
},
|
||||
{
|
||||
id: 'butcher',
|
||||
name: 'Boucher',
|
||||
food: 5,
|
||||
war: 3,
|
||||
health: 1,
|
||||
diplo: 1,
|
||||
description: 'Maître des viandes, il nourrit le peuple mais soigne peu.'
|
||||
},
|
||||
{
|
||||
id: 'doctor',
|
||||
name: 'Médecin',
|
||||
food: 2,
|
||||
war: 1,
|
||||
health: 6,
|
||||
diplo: 3,
|
||||
description: 'Expert en guérison, indispensable à la survie du royaume.'
|
||||
},
|
||||
{
|
||||
id: 'apothecary',
|
||||
name: 'Apothicaire',
|
||||
food: 3,
|
||||
war: 1,
|
||||
health: 5,
|
||||
diplo: 3,
|
||||
description: 'Prépare potions et remèdes, soutien précieux en temps de crise.'
|
||||
},
|
||||
{
|
||||
id: 'jester',
|
||||
name: 'Fou du Roi',
|
||||
food: 1,
|
||||
war: 1,
|
||||
health: 2,
|
||||
diplo: 6,
|
||||
description: 'Sa sagesse cachée derrière la bouffonnerie fait des miracles diplomatiques.'
|
||||
},
|
||||
{
|
||||
id: 'assassin',
|
||||
name: 'Assassin',
|
||||
food: 1,
|
||||
war: 6,
|
||||
health: 1,
|
||||
diplo: 2,
|
||||
description: 'Frappe dans l\'ombre, inarrêtable au combat mais inutile ailleurs.'
|
||||
},
|
||||
{
|
||||
id: 'peasant',
|
||||
name: 'Paysan',
|
||||
food: 6,
|
||||
war: 2,
|
||||
health: 2,
|
||||
diplo: 1,
|
||||
description: 'Nourrit tout le royaume, force vitale de toute civilisation.'
|
||||
},
|
||||
{
|
||||
id: 'executioner',
|
||||
name: 'Bourreau',
|
||||
food: 2,
|
||||
war: 5,
|
||||
health: 1,
|
||||
diplo: 2,
|
||||
description: 'Bras armé de la justice royale, redouté de tous.'
|
||||
},
|
||||
{
|
||||
id: 'knight',
|
||||
name: 'Chevalier',
|
||||
food: 3,
|
||||
war: 5,
|
||||
health: 2,
|
||||
diplo: 2,
|
||||
description: 'Défenseur du royaume, vaillant sur le champ de bataille.'
|
||||
},
|
||||
{
|
||||
id: 'merchant',
|
||||
name: 'Marchand',
|
||||
food: 4,
|
||||
war: 1,
|
||||
health: 2,
|
||||
diplo: 5,
|
||||
description: 'Ses routes commerciales enrichissent et lient les royaumes.'
|
||||
},
|
||||
{
|
||||
id: 'bishop',
|
||||
name: 'Évêque',
|
||||
food: 2,
|
||||
war: 1,
|
||||
health: 4,
|
||||
diplo: 5,
|
||||
description: 'Voix de l\'Église, il guérit les âmes et négocie la paix.'
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* Couleurs des 4 équipes (teinte principale de la carte)
|
||||
*/
|
||||
const TEAM_COLORS = {
|
||||
0: { name: 'Écarlate', hex: 0xcc2200, css: '#cc2200' },
|
||||
1: { name: 'Azur', hex: 0x1155cc, css: '#1155cc' },
|
||||
2: { name: 'Émeraude', hex: 0x1a7a2e, css: '#1a7a2e' },
|
||||
3: { name: 'Or', hex: 0xb8860b, css: '#b8860b' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Attributs visuels fixes (couleur de chaque coin)
|
||||
*/
|
||||
const ATTR_COLORS = {
|
||||
food: { hex: 0xf0c030, css: '#f0c030', label: 'Nourriture', corner: 'topLeft' },
|
||||
war: { hex: 0xdd2222, css: '#dd2222', label: 'Guerre', corner: 'topRight' },
|
||||
health: { hex: 0x22aa44, css: '#22aa44', label: 'Santé', corner: 'bottomLeft' },
|
||||
diplo: { hex: 0x3399ee, css: '#3399ee', label: 'Diplomatie', corner: 'bottomRight' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le deck complet (les 12 personnages) pour une équipe donnée
|
||||
*/
|
||||
function getDeckForTeam(teamIndex) {
|
||||
return CHARACTERS.map(c => ({
|
||||
...c,
|
||||
teamIndex,
|
||||
teamColor: TEAM_COLORS[teamIndex]
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribue une main de N cartes par joueur depuis un deck mélangé
|
||||
*/
|
||||
function shuffle(arr) {
|
||||
const a = [...arr]
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
@@ -35,17 +165,13 @@ function shuffle(arr) {
|
||||
return a
|
||||
}
|
||||
|
||||
function createDeck() {
|
||||
return shuffle([...BASE_DECK, ...BASE_DECK]) // 2 exemplaires
|
||||
}
|
||||
|
||||
function dealHands(playerCount, handSize = 5) {
|
||||
const deck = createDeck()
|
||||
function dealHands(playerCount, handSize = 4) {
|
||||
const hands = []
|
||||
for (let i = 0; i < playerCount; i++) {
|
||||
hands.push(deck.splice(0, handSize))
|
||||
const deck = shuffle(getDeckForTeam(i))
|
||||
hands.push(deck.slice(0, handSize))
|
||||
}
|
||||
return { hands, remaining: deck }
|
||||
return hands
|
||||
}
|
||||
|
||||
module.exports = { createDeck, dealHands, BASE_DECK }
|
||||
module.exports = { CHARACTERS, TEAM_COLORS, ATTR_COLORS, getDeckForTeam, dealHands }
|
||||
+42
-24
@@ -1,7 +1,7 @@
|
||||
const express = require('express')
|
||||
const http = require('http')
|
||||
const http = require('http')
|
||||
const { Server } = require('socket.io')
|
||||
const cors = require('cors')
|
||||
const cors = require('cors')
|
||||
const { RoomManager } = require('./game/RoomManager')
|
||||
|
||||
const app = express()
|
||||
@@ -16,8 +16,9 @@ const io = new Server(server, {
|
||||
const rooms = new RoomManager()
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`[+] Connexion: ${socket.id}`)
|
||||
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)
|
||||
@@ -28,13 +29,14 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
|
||||
socket.join(roomId)
|
||||
socket.data.roomId = roomId
|
||||
socket.data.roomId = roomId
|
||||
socket.data.playerName = playerName
|
||||
|
||||
io.to(roomId).emit('room-update', { players: room.getPlayersInfo() })
|
||||
console.log(`[room:${roomId}] ${playerName} rejoint (${room.playerCount}/4)`)
|
||||
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
|
||||
@@ -42,18 +44,21 @@ io.on('connection', (socket) => {
|
||||
room.startGame()
|
||||
io.to(roomId).emit('game-start')
|
||||
|
||||
// Envoyer l'état initial à chaque joueur avec sa main
|
||||
room.players.forEach((player) => {
|
||||
// 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))
|
||||
})
|
||||
|
||||
// ─── Jouer une carte ─────────────────────────────────────────────────────────
|
||||
socket.on('play-card', ({ roomId, cardId }) => {
|
||||
const room = rooms.get(roomId)
|
||||
if (!room) return
|
||||
@@ -64,40 +69,52 @@ io.on('connection', (socket) => {
|
||||
return
|
||||
}
|
||||
|
||||
// Mise à jour de phase pour tous
|
||||
// Informer tous que quelqu'un a joué (sans révéler la carte)
|
||||
io.to(roomId).emit('game-state', {
|
||||
players: room.getPlayersInfo(),
|
||||
players: room.getPlayersInfo(),
|
||||
currentTurn: room.currentTurn,
|
||||
maxTurns: room.maxTurns,
|
||||
phase: room.phase
|
||||
maxTurns: room.maxTurns,
|
||||
phase: room.phase
|
||||
})
|
||||
|
||||
// Tous les joueurs ont joué → résolution
|
||||
// Tous les joueurs ont joué → résolution automatique
|
||||
if (room.allPlayersPlayed()) {
|
||||
const roundResult = room.resolveRound()
|
||||
io.to(roomId).emit('game-state', {
|
||||
players: room.getPlayersInfo(),
|
||||
currentTurn: room.currentTurn,
|
||||
maxTurns: room.maxTurns,
|
||||
phase: 'resolve',
|
||||
playedCards: roundResult.playedCards
|
||||
console.log(`[${roomId}] Tour ${room.currentTurn} résolu`)
|
||||
|
||||
// Révéler les cartes jouées et les résultats
|
||||
io.to(roomId).emit('round-result', {
|
||||
playedCards: roundResult.playedCards,
|
||||
attrResults: roundResult.attrResults,
|
||||
message: roundResult.message
|
||||
})
|
||||
|
||||
io.to(roomId).emit('round-result', { message: roundResult.message })
|
||||
// Mise à jour scores
|
||||
io.to(roomId).emit('game-state', {
|
||||
players: room.getPlayersInfo(),
|
||||
currentTurn: room.currentTurn,
|
||||
maxTurns: room.maxTurns,
|
||||
phase: 'resolve'
|
||||
})
|
||||
|
||||
// 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 {
|
||||
room.nextTurn()
|
||||
room.players.forEach((player) => {
|
||||
// Distribuer la nouvelle main à chaque joueur
|
||||
room.players.forEach(player => {
|
||||
io.to(player.socketId).emit('game-state', room.getStateFor(player.socketId))
|
||||
})
|
||||
}
|
||||
}, 3000)
|
||||
}, 3500)
|
||||
}
|
||||
})
|
||||
|
||||
// ─── Déconnexion ─────────────────────────────────────────────────────────────
|
||||
socket.on('disconnect', () => {
|
||||
const roomId = socket.data.roomId
|
||||
if (roomId) {
|
||||
@@ -105,13 +122,14 @@ io.on('connection', (socket) => {
|
||||
if (room) {
|
||||
room.removePlayer(socket.id)
|
||||
io.to(roomId).emit('room-update', { players: room.getPlayersInfo() })
|
||||
if (room.playerCount === 0) rooms.delete(roomId)
|
||||
}
|
||||
}
|
||||
console.log(`[-] Déconnexion: ${socket.id}`)
|
||||
console.log(`[-] ${socket.id}`)
|
||||
})
|
||||
})
|
||||
|
||||
const PORT = process.env.PORT || 3000
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Serveur Regnum démarré sur le port ${PORT}`)
|
||||
console.log(`Regnum server → http://localhost:${PORT}`)
|
||||
})
|
||||
Reference in new issue
Block a user