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:
1 parent
da06ecaf2d
commit
6a1c800253
37 files changed
+3558
-188
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user