From 4d091024649060eb94250494c11f35a1c0c3a9c9 Mon Sep 17 00:00:00 2001 From: "kevin.lhereec" Date: Mon, 17 Aug 2026 14:25:32 +0200 Subject: [PATCH] fix: use BitmapMask inside container so image follows card during animations --- client/src/game/objects/CardObject.js | 122 ++++++++++++-------------- 1 file changed, 57 insertions(+), 65 deletions(-) diff --git a/client/src/game/objects/CardObject.js b/client/src/game/objects/CardObject.js index 506d4e1..5d7eddc 100644 --- a/client/src/game/objects/CardObject.js +++ b/client/src/game/objects/CardObject.js @@ -19,6 +19,7 @@ export class CardObject extends Phaser.GameObjects.Container { this.cardData = cardData this._selected = false this._baseY = y + this._maskImg = null // référence au masque pour le détruire proprement this._build() scene.add.existing(this) } @@ -38,67 +39,61 @@ export class CardObject extends Phaser.GameObjects.Container { this.add(shadow) // ── 2. Fond couleur équipe ──────────────────────────────────────────────── - const bgColor = this._darken(teamHex, 0.25) const bg = this.scene.add.graphics() - bg.fillStyle(bgColor) + bg.fillStyle(this._darken(teamHex, 0.25)) bg.fillRoundedRect(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H, RADIUS) this.add(bg) - // ── 3. Image portrait via RenderTexture (reste solidaire du container) ─── + // ── 3. Image portrait ───────────────────────────────────────────────────── if (hasTexture) { - // RenderTexture aux dimensions de la carte — clippe naturellement - const rt = this.scene.add.renderTexture( - -CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H - ) - - // Dessiner le fond arrondi dans la RT (pour que les coins soient propres) - const rtBg = this.scene.make.graphics({ x: 0, y: 0, add: false }) - rtBg.fillStyle(bgColor) - rtBg.fillRoundedRect(0, 0, CARD_W, CARD_H, RADIUS) - rt.draw(rtBg, 0, 0) - rtBg.destroy() - - // Calculer le scale "cover" et le centrage dans la RT const frame = this.scene.textures.get(textureKey).getSourceImage() const scaleX = CARD_W / frame.width const scaleY = CARD_H / frame.height const scale = Math.max(scaleX, scaleY) - const dw = frame.width * scale - const dh = frame.height * scale - const ox = (CARD_W - dw) / 2 - const oy = (CARD_H - dh) / 2 - // Créer un Image temporaire pour dessiner dans la RT - const tmpImg = this.scene.make.image({ key: textureKey, add: false }) - tmpImg.setScale(scale) - rt.draw(tmpImg, ox + dw / 2, oy + dh / 2) - tmpImg.destroy() + const img = this.scene.add.image(0, 0, textureKey) + img.setScale(scale) + this.add(img) - // Dégradé haut (pour lisibilité des badges) - const gradT = this.scene.make.graphics({ x: 0, y: 0, add: false }) - gradT.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0.75, 0.75, 0, 0) - gradT.fillRect(0, 0, CARD_W, BADGE + 10) - rt.draw(gradT, 0, 0) - gradT.destroy() + // Masque arrondi rendu dans une texture offline, puis appliqué comme BitmapMask + // Le BitmapMask suit le container automatiquement car on l'attache à un objet du container + const maskKey = `mask-card` + if (!this.scene.textures.exists(maskKey)) { + const maskGfx = this.scene.make.graphics({ x: 0, y: 0, add: false }) + maskGfx.fillStyle(0xffffff) + maskGfx.fillRoundedRect(0, 0, CARD_W, CARD_H, RADIUS) + maskGfx.generateTexture(maskKey, CARD_W, CARD_H) + maskGfx.destroy() + } - // Dégradé bas (pour lisibilité du titre) - const gradB = this.scene.make.graphics({ x: 0, y: 0, add: false }) - gradB.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0, 0, 0.85, 0.85) - gradB.fillRect(0, CARD_H - TITLE_H - 12, CARD_W, TITLE_H + 12) - rt.draw(gradB, 0, 0) - gradB.destroy() + // L'image masque est ajoutée AU container → elle suit ses mouvements + this._maskImg = this.scene.add.image(0, 0, maskKey) + this._maskImg.setVisible(false) + this.add(this._maskImg) - this.add(rt) + img.setMask(this._maskImg.createBitmapMask()) } - // ── 4. Bordure couleur équipe ───────────────────────────────────────────── + // ── 4. Dégradé haut (lisibilité badges) ────────────────────────────────── + const gradT = this.scene.add.graphics() + gradT.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0.75, 0.75, 0, 0) + gradT.fillRect(-CARD_W / 2, -CARD_H / 2, CARD_W, BADGE + 10) + this.add(gradT) + + // ── 5. Dégradé bas (lisibilité titre) ──────────────────────────────────── + const gradB = this.scene.add.graphics() + gradB.fillGradientStyle(0x000000, 0x000000, 0x000000, 0x000000, 0, 0, 0.85, 0.85) + gradB.fillRect(-CARD_W / 2, CARD_H / 2 - TITLE_H - 12, CARD_W, TITLE_H + 12) + this.add(gradB) + + // ── 6. Bordure couleur équipe ───────────────────────────────────────────── const border = this.scene.add.graphics() border.lineStyle(2.5, teamHex, 1) border.strokeRoundedRect(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H, RADIUS) this.add(border) - // ── 5. Titre en BAS ─────────────────────────────────────────────────────── - const nameText = this.scene.add.text( + // ── 7. Titre en BAS ─────────────────────────────────────────────────────── + this.add(this.scene.add.text( 0, CARD_H / 2 - TITLE_H / 2, this.cardData.name, { fontSize: '12px', @@ -106,19 +101,17 @@ export class CardObject extends Phaser.GameObjects.Container { fontStyle: 'bold', color: '#f5e6c8', stroke: '#000000', - strokeThickness: 3, - backgroundColor: undefined // pas de fond blanc + strokeThickness: 3 } - ).setOrigin(0.5, 0.5) - this.add(nameText) + ).setOrigin(0.5, 0.5)) - // ── 6. Badges attributs dans les 4 coins ───────────────────────────────── + // ── 8. Badges attributs dans les 4 coins ───────────────────────────────── 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') - // ── 7. Interactivité ────────────────────────────────────────────────────── + // ── 9. Interactivité ────────────────────────────────────────────────────── this.setSize(CARD_W, CARD_H) this.setInteractive({ cursor: 'pointer' }) this.on('pointerover', this._onHover, this) @@ -157,15 +150,13 @@ export class CardObject extends Phaser.GameObjects.Container { fontStyle: 'bold', color: '#ffffff', stroke: '#000000', - strokeThickness: 2, - backgroundColor: undefined + strokeThickness: 2 }).setOrigin(0.5, 0.5)) this.add(this.scene.add.text(cx, cy + 9, label, { - fontSize: '6px', - fontFamily: 'Arial, sans-serif', - color: '#ffffffdd', - backgroundColor: undefined + fontSize: '6px', + fontFamily:'Arial, sans-serif', + color: '#ffffffdd' }).setOrigin(0.5, 0.5)) } @@ -173,17 +164,13 @@ export class CardObject extends Phaser.GameObjects.Container { _onHover() { if (this._selected) return - this.scene.tweens.add({ - targets: this, y: this._baseY - 18, duration: 120, ease: 'Power2' - }) + this.scene.tweens.add({ targets: this, y: this._baseY - 18, duration: 120, ease: 'Power2' }) this.setDepth(10) } _onOut() { if (this._selected) return - this.scene.tweens.add({ - targets: this, y: this._baseY, duration: 120, ease: 'Power2' - }) + this.scene.tweens.add({ targets: this, y: this._baseY, duration: 120, ease: 'Power2' }) this.setDepth(0) } @@ -199,17 +186,22 @@ export class CardObject extends Phaser.GameObjects.Container { select() { this._selected = true this.setDepth(20) - this.scene.tweens.add({ - targets: this, y: this._baseY - 28, duration: 150, ease: 'Back.Out' - }) + this.scene.tweens.add({ targets: this, y: this._baseY - 28, duration: 150, ease: 'Back.Out' }) } deselect() { this._selected = false this.setDepth(0) - this.scene.tweens.add({ - targets: this, y: this._baseY, duration: 150, ease: 'Power2' - }) + this.scene.tweens.add({ targets: this, y: this._baseY, duration: 150, ease: 'Power2' }) + } + + destroy(fromScene) { + // Nettoyer le masque proprement + if (this._maskImg) { + this._maskImg.destroy() + this._maskImg = null + } + super.destroy(fromScene) } // ── Utilitaires ───────────────────────────────────────────────────────────────