feat: système d'objets achetables (boutique + effets permanents) et refonte visuelle

- Nouveaux personnages illustrés (12 PNG remplacés dans client/public/cards)
- 4 objets achetables (Trébuchet, Charrue, Trousse du Médecin, Sceau Royal)
  avec effets permanents (double_gain, guaranteed_gain, copy_best_in_game),
  stock unique par partie, priorité d'achat tournante entre joueurs
- Boutique en colonne à gauche du plateau : icônes compactes, carte détaillée
  agrandie au survol (taille découplée du displayScale du jeton), bouton
  ACHETER actif/grisé selon la jauge du joueur, retrait immédiat de la
  colonne une fois l'objet acheté (les autres remontent pour combler)
- Objets achetés affichés "posés sur la table" : à côté de la main pour soi,
  sous le slot de jeu pour les adversaires
- Icône du premier joueur (priorité d'achat) dans le HUD Vue
- Refonte des cartes personnage (CardObject) : portrait plein cadre épuré,
  valeurs d'attribut discrètes aux 4 coins
- Fix removePlayer : nettoyage de pendingBuys/pendingPlays et recalage de
  firstPlayerIndex à la déconnexion d'un joueur
- 77 tests unitaires serveur (node:test) : cards, items, Room, RoomManager
- README mis à jour avec les règles complètes du jeu
- .gitignore : exclusion de .idea/ et .serena/ (artefacts d'outils locaux)
This commit is contained in:
Kevin L'Hereec committed 2026-08-21 14:53:58 +02:00
1 parent da06ecaf2d
commit 6a1c800253
37 files changed
+3558 -188

No files matched your search

+73 -5
View File
@@ -1,4 +1,7 @@
import Phaser from 'phaser'
import { CARD_W, CARD_H, RADIUS } from '../objects/cardLayout'
import { ITEM_CARD_W, ITEM_IMAGE_H, ITEM_RADIUS } from '../objects/itemLayout'
import { ITEMS } from '../items'
const CARD_IMAGES = [
{ id: 'queen', file: 'reine.png' },
@@ -15,10 +18,6 @@ const CARD_IMAGES = [
{ id: 'bishop', file: 'eveque.png' }
]
const CARD_W = 130
const CARD_H = 190
const RADIUS = 10
export class BootScene extends Phaser.Scene {
constructor() {
super('BootScene')
@@ -44,10 +43,15 @@ export class BootScene extends Phaser.Scene {
barFill.destroy()
})
// Charger les images brutes
// Charger les images brutes des personnages
CARD_IMAGES.forEach(({ id, file }) => {
this.load.image(`raw-${id}`, `/cards/${file}`)
})
// Charger les images brutes des objets
ITEMS.forEach(({ id, image }) => {
this.load.image(`raw-item-${id}`, `/items/${image}`)
})
}
create() {
@@ -56,6 +60,12 @@ export class BootScene extends Phaser.Scene {
this._generateClippedTexture(id)
})
// Pour chaque objet, générer une texture portrait (zone haute uniquement,
// le bas de la carte objet est réservé au texte d'effet côté ItemObject)
ITEMS.forEach(({ id }) => {
this._generateItemTexture(id)
})
this.scene.start('GameScene')
}
@@ -121,4 +131,62 @@ export class BootScene extends Phaser.Scene {
// Enregistrer comme texture Phaser
this.textures.addCanvas(outKey, canvas)
}
/**
* Crée une texture `item-{id}` de dimensions ITEM_CARD_W × ITEM_IMAGE_H
* (uniquement la zone portrait en haut de la carte objet, coins arrondis
* en haut seulement — le bas de la carte est une zone de texte gérée par
* ItemObject, pas par cette texture).
*/
_generateItemTexture(id) {
const rawKey = `raw-item-${id}`
const outKey = `item-${id}`
if (!this.textures.exists(rawKey)) return
if (this.textures.exists(outKey)) return
const srcImage = this.textures.get(rawKey).getSourceImage()
const w = ITEM_CARD_W
const h = ITEM_IMAGE_H
const r = ITEM_RADIUS
const canvas = document.createElement('canvas')
canvas.width = w
canvas.height = h
const ctx = canvas.getContext('2d')
// Chemin arrondi seulement en haut (bas droit, ce sera raccordé à la
// zone de texte qui suit immédiatement en dessous dans ItemObject)
ctx.beginPath()
ctx.moveTo(r, 0)
ctx.lineTo(w - r, 0)
ctx.quadraticCurveTo(w, 0, w, r)
ctx.lineTo(w, h)
ctx.lineTo(0, h)
ctx.lineTo(0, r)
ctx.quadraticCurveTo(0, 0, r, 0)
ctx.closePath()
ctx.clip()
// Dessiner l'image en mode "cover" (ratio conservé, centré)
const scaleX = w / srcImage.width
const scaleY = h / srcImage.height
const scale = Math.max(scaleX, scaleY)
const dw = srcImage.width * scale
const dh = srcImage.height * scale
const ox = (w - dw) / 2
const oy = (h - dh) / 2
ctx.drawImage(srcImage, ox, oy, dw, dh)
// Léger dégradé bas pour raccorder proprement à la zone de texte
const gBot = ctx.createLinearGradient(0, h - 24, 0, h)
gBot.addColorStop(0, 'rgba(0,0,0,0)')
gBot.addColorStop(1, 'rgba(0,0,0,0.55)')
ctx.fillStyle = gBot
ctx.fillRect(0, h - 24, w, 24)
this.textures.addCanvas(outKey, canvas)
}
}