From 6e384d598501ddf8ba6e5d1c0099175d132193f4 Mon Sep 17 00:00:00 2001 From: "kevin.lhereec" Date: Mon, 17 Aug 2026 14:15:43 +0200 Subject: [PATCH] fix: card image cover ratio, title at bottom, badges above image --- client/src/game/objects/CardObject.js | 156 +++++++++++++------------- 1 file changed, 80 insertions(+), 76 deletions(-) diff --git a/client/src/game/objects/CardObject.js b/client/src/game/objects/CardObject.js index 5eb52fa..e3eac28 100644 --- a/client/src/game/objects/CardObject.js +++ b/client/src/game/objects/CardObject.js @@ -1,6 +1,6 @@ import Phaser from 'phaser' -// Couleurs des 4 attributs (identiques au serveur) +// Couleurs des 4 attributs const ATTR = { food: { color: 0xf0c030, label: 'NOU' }, war: { color: 0xdd2222, label: 'GUE' }, @@ -8,101 +8,108 @@ const ATTR = { diplo: { color: 0x3399ee, label: 'DIP' } } -const CARD_W = 130 -const CARD_H = 190 -const CORNER = 28 // taille du badge de coin -const RADIUS = 10 +const CARD_W = 130 +const CARD_H = 190 +const RADIUS = 10 +const BADGE = 30 // taille du badge de coin +const TITLE_H = 26 // hauteur du bandeau titre en bas 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() scene.add.existing(this) } _build() { - const teamHex = this.cardData.teamColor?.hex ?? 0x888888 + const teamHex = this.cardData.teamColor?.hex ?? 0x888888 const textureKey = `card-${this.cardData.id}` const hasTexture = this.scene.textures.exists(textureKey) - // --- Ombre portée --- + // ── 1. Ombre ───────────────────────────────────────────────────────────── const shadow = this.scene.add.graphics() - shadow.fillStyle(0x000000, 0.45) - shadow.fillRoundedRect(-CARD_W / 2 + 4, -CARD_H / 2 + 4, CARD_W, CARD_H, RADIUS) + shadow.fillStyle(0x000000, 0.5) + shadow.fillRoundedRect(-CARD_W / 2 + 5, -CARD_H / 2 + 5, CARD_W, CARD_H, RADIUS) this.add(shadow) - // --- Fond coloré équipe --- + // ── 2. Fond couleur équipe ──────────────────────────────────────────────── const bg = this.scene.add.graphics() - bg.fillStyle(this._darken(teamHex, 0.25)) + bg.fillStyle(this._darken(teamHex, 0.2)) bg.fillRoundedRect(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H, RADIUS) this.add(bg) - // --- Image du personnage (zone portrait) --- + // ── 3. Image portrait (ratio conservé, recadré au centre) ───────────────── if (hasTexture) { - const img = this.scene.add.image(0, -10, textureKey) - // Ajuster pour remplir la zone portrait en conservant le ratio - const portraitH = CARD_H - 50 // laisser place au nom (haut) et badges (coins) - const portraitW = CARD_W - 4 - img.setDisplaySize(portraitW, portraitH) - img.setAlpha(0.92) - // Masque arrondi via crop + clip (simulé via alpha sur les bords) - this.add(img) + const img = this.scene.add.image(0, 0, textureKey) - // Overlay dégradé en bas pour lisibilité du texte - const overlay = this.scene.add.graphics() - overlay.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0, 0, 0.7, 0.7) - overlay.fillRect(-CARD_W / 2, CARD_H / 2 - 55, CARD_W, 55) - this.add(overlay) + // Calculer le scale pour couvrir toute la carte sans déformer (cover) + const scaleX = CARD_W / img.width + const scaleY = CARD_H / img.height + const scale = Math.max(scaleX, scaleY) + img.setScale(scale) + + // Décaler légèrement vers le haut pour centrer le visage (haut du portrait) + img.setY(-CARD_H * 0.05) + img.setAlpha(0.9) + + // Masque rectangulaire pour clipper l'image dans les bords de la carte + const mask = this.scene.add.graphics() + mask.fillStyle(0xffffff) + mask.fillRoundedRect( + this.x - CARD_W / 2, + this.y - CARD_H / 2, + CARD_W, CARD_H, RADIUS + ) + img.setMask(mask.createGeometryMask()) + + this.add(img) } else { - // Fallback : initiale si l'image n'est pas chargée - const initial = this.scene.add.text(0, -10, this.cardData.name[0], { - fontSize: '60px', - fontFamily: 'Georgia, serif', - color: '#ffffff', - alpha: 0.12 - }).setOrigin(0.5, 0.5) + // Fallback : initiale + const initial = this.scene.add.text(0, 0, this.cardData.name[0], { + fontSize: '70px', fontFamily: 'Georgia', color: '#ffffff', alpha: 0.1 + }).setOrigin(0.5) this.add(initial) } - // --- Bordure couleur équipe --- + // ── 4. Overlay dégradé bas (pour lisibilité du titre) ──────────────────── + const gradBot = this.scene.add.graphics() + gradBot.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0, 0, 0.85, 0.85) + gradBot.fillRect(-CARD_W / 2, CARD_H / 2 - TITLE_H - 10, CARD_W, TITLE_H + 10) + this.add(gradBot) + + // ── 5. Overlay dégradé haut (pour lisibilité des badges) ───────────────── + const gradTop = this.scene.add.graphics() + gradTop.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0.7, 0.7, 0, 0) + gradTop.fillRect(-CARD_W / 2, -CARD_H / 2, CARD_W, BADGE + 8) + this.add(gradTop) + + // ── 6. Bordure couleur équipe ───────────────────────────────────────────── const border = this.scene.add.graphics() - border.lineStyle(2, teamHex, 1) + border.lineStyle(2.5, teamHex, 1) border.strokeRoundedRect(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H, RADIUS) this.add(border) - // --- Bandeau nom (haut) --- - const nameBg = this.scene.add.graphics() - nameBg.fillStyle(0x000000, 0.65) - nameBg.fillRoundedRect(-CARD_W / 2, -CARD_H / 2, CARD_W, 24, { tl: RADIUS, tr: RADIUS, bl: 0, br: 0 }) - this.add(nameBg) - - const nameText = this.scene.add.text(0, -CARD_H / 2 + 12, this.cardData.name, { - fontSize: '12px', + // ── 7. Titre en BAS ─────────────────────────────────────────────────────── + const nameText = this.scene.add.text(0, CARD_H / 2 - TITLE_H / 2, this.cardData.name, { + fontSize: '13px', fontFamily: 'Georgia, serif', + fontStyle: 'bold', color: '#f5e6c8', stroke: '#000000', - strokeThickness: 2, - fontStyle: 'bold' + strokeThickness: 3 }).setOrigin(0.5, 0.5) this.add(nameText) - // --- 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') + // ── 8. Badges attributs dans les 4 coins (AU-DESSUS de l'image) ─────────── + this._addCornerBadge('food', -CARD_W / 2, -CARD_H / 2, 'topLeft') + this._addCornerBadge('war', CARD_W / 2, -CARD_H / 2, 'topRight') + this._addCornerBadge('health', -CARD_W / 2, CARD_H / 2, 'bottomLeft') + this._addCornerBadge('diplo', CARD_W / 2, CARD_H / 2, 'bottomRight') - // --- Interactivité --- + // ── 9. Interactivité ────────────────────────────────────────────────────── this.setSize(CARD_W, CARD_H) this.setInteractive({ cursor: 'pointer' }) this.on('pointerover', this._onHover, this) @@ -110,53 +117,50 @@ export class CardObject extends Phaser.GameObjects.Container { this.on('pointerdown', this._onDown, this) } - /** - * Badge carré coloré dans un coin, avec la valeur de l'attribut - */ _addCornerBadge(attr, bx, by, corner) { const { color, label } = ATTR[attr] const value = this.cardData[attr] - const S = CORNER + const S = BADGE + // Position du carré 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 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 } + topLeft: { tl: RADIUS, tr: 2, bl: 2, br: RADIUS / 2 }, + topRight: { tl: 2, tr: RADIUS, bl: RADIUS / 2, br: 2 }, + bottomLeft: { tl: 2, tr: RADIUS / 2, bl: RADIUS, br: 2 }, + bottomRight: { tl: RADIUS / 2, tr: 2, bl: 2, br: RADIUS } } const g = this.scene.add.graphics() - g.fillStyle(color, 0.92) + g.fillStyle(color, 0.95) g.fillRoundedRect(rx, ry, S, S, cornerRadii[corner]) this.add(g) const cx = rx + S / 2 const cy = ry + S / 2 - 3 - const valText = this.scene.add.text(cx, cy, String(value), { + // Valeur + this.add(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) + }).setOrigin(0.5, 0.5)) - const lblText = this.scene.add.text(cx, cy + 8, label, { + // Label + this.add(this.scene.add.text(cx, cy + 9, label, { fontSize: '6px', fontFamily: 'Arial, sans-serif', - color: '#ffffff', - alpha: 0.9 - }).setOrigin(0.5, 0.5) - this.add(lblText) + color: '#ffffffdd' + }).setOrigin(0.5, 0.5)) } - // --- Interactions --- + // ── Interactions ───────────────────────────────────────────────────────────── _onHover() { if (this._selected) return @@ -191,7 +195,7 @@ export class CardObject extends Phaser.GameObjects.Container { this.scene.tweens.add({ targets: this, y: this._baseY, duration: 150, ease: 'Power2' }) } - // --- Utilitaires --- + // ── Utilitaires ─────────────────────────────────────────────────────────────── _darken(hex, factor) { const r = Math.floor(((hex >> 16) & 0xff) * factor)