diff --git a/src/scenes/GameMenuScene.js b/src/scenes/GameMenuScene.js index 4e9a2f1..a335ea6 100644 --- a/src/scenes/GameMenuScene.js +++ b/src/scenes/GameMenuScene.js @@ -2,6 +2,7 @@ import * as Phaser from 'phaser'; import { GAME_HEIGHT, GAME_WIDTH, COLORS } from '../config.js'; import { api } from '../services/api.js'; import { Button } from '../ui/Button.js'; +import { Plaque } from '../ui/Plaque.js'; import { addFullscreenButton } from '../ui/FullscreenButton.js'; import { playMenuMusic, stopMenuMusic } from '../ui/MenuMusic.js'; import { TutorialModal } from '../ui/TutorialModal.js'; @@ -38,8 +39,13 @@ export default class GameMenuScene extends Phaser.Scene { fontSize: '64px', color: COLORS.textHex, }).setOrigin(0.5).setDepth(1); + titleText.setLetterSpacing(2); + titleText.setShadow(0, 4, 'rgba(0,0,0,0.9)', 8); this._titleText = titleText; - this._titleBg = this.add.rectangle(cx, 60, titleText.width + 64, titleText.height + 28, 0x000000, 0.7); + const plateW = titleText.width + 64; + const plateH = titleText.height + 28; + this._titleBg = new Plaque(this, plateW, plateH).setPosition(cx, 60).setDepth(0.5); + this._titlePlateW = plateW; const loadingText = this.add.text(cx, 540, 'Loading game list…', { fontSize: '24px', color: COLORS.mutedHex, @@ -134,7 +140,15 @@ export default class GameMenuScene extends Phaser.Scene { if (this._hintText) { this._hintText.destroy(); this._hintText = null; } if (this._titleText) { this._titleText.setText('Choose a game'); - this._titleBg.setSize(this._titleText.width + 64, this._titleText.height + 28); + const newW = this._titleText.width + 64; + const newH = this._titleText.height + 28; + if (this._titlePlateW && Math.abs(this._titlePlateW - newW) > 1) { + this._titleBg.animateSize(newW, newH, 240, 'Quad.easeOut'); + this._titlePlateW = newW; + } + // Little pop so the label swap reads as intentional. + this._titleText.setScale(0.9); + this.tweens.add({ targets: this._titleText, scaleX: 1, scaleY: 1, duration: 240, ease: 'Back.easeOut' }); } for (const [k, btn] of Object.entries(this._tabs)) { btn.setActive(k === key); diff --git a/src/scenes/LandingScene.js b/src/scenes/LandingScene.js index ee83657..6febc39 100644 --- a/src/scenes/LandingScene.js +++ b/src/scenes/LandingScene.js @@ -2,6 +2,7 @@ import * as Phaser from 'phaser'; import { GAME_HEIGHT, GAME_WIDTH, COLORS } from '../config.js'; import { auth } from '../services/auth.js'; import { Button } from '../ui/Button.js'; +import { Plaque } from '../ui/Plaque.js'; import { addFullscreenButton } from '../ui/FullscreenButton.js'; import { playMenuMusic } from '../ui/MenuMusic.js'; @@ -69,6 +70,7 @@ export default class LandingScene extends Phaser.Scene { color: COLORS.accentHex, }).setOrigin(0.5).setDepth(1); this._welcomeText = welcomeText; + welcomeText.setShadow(0, 3, 'rgba(0,0,0,0.8)', 5); const hasAvatar = !!user?.avatarPath; const totalW = hasAvatar ? avatarR * 2 + avatarGap + welcomeText.width : welcomeText.width; @@ -76,7 +78,8 @@ export default class LandingScene extends Phaser.Scene { welcomeText.setX(hasAvatar ? groupLeft + avatarR * 2 + avatarGap + welcomeText.width / 2 : cx); - this._welcomeBg = this.add.rectangle(cx, y, totalW + pad.x * 2, welcomeText.height + pad.y * 2, 0x000000, 0.45); + this._welcomeBg = new Plaque(this, totalW + pad.x * 2, welcomeText.height + pad.y * 2, { radius: 16 }) + .setPosition(cx, y); if (hasAvatar) { const avatarCx = groupLeft + avatarR; diff --git a/src/ui/Plaque.js b/src/ui/Plaque.js new file mode 100644 index 0000000..24b15b7 --- /dev/null +++ b/src/ui/Plaque.js @@ -0,0 +1,62 @@ +import * as Phaser from 'phaser'; +import { COLORS } from '../config.js'; + +// A rounded, brass-trimmed plaque in the same design language as the +// category tabs. Use instead of a flat black rectangle behind title text. +export class Plaque extends Phaser.GameObjects.Graphics { + constructor(scene, width = 300, height = 90, options = {}) { + super(scene, false); + const { + fill = 0x241c10, + fillAlpha = 0.92, + border = COLORS.accent, + radius = 18, + } = options; + this._opts = { fill, fillAlpha, border, radius }; + this._w = width; + this._h = height; + this.postFX.addShadow(0, 3, 0.004, 1.5, 0x000000, 8, 0.65); + this.redraw(width, height); + scene.add.existing(this); + } + + redraw(w, h) { + this._w = w; + this._h = h; + const { fill, fillAlpha, border, radius } = this._opts; + const r = Math.max(4, Math.min(radius, w / 2, h / 2)); + this.clear(); + this.fillStyle(fill, fillAlpha); + this.fillRoundedRect(-w / 2, -h / 2, w, h, r); + // Faint warm highlight just inside the top edge reads as a raised plate. + this.lineStyle(2, 0xfff6dd, 0.16); + this.lineBetween(-w / 2 + r + 4, -h / 2 + 4, w / 2 - r - 4, -h / 2 + 4); + this.lineStyle(2, border, 1); + this.strokeRoundedRect(-w / 2, -h / 2, w, h, r); + } + + // Animate the plate to a new size. Graphics in this Phaser build exposes + // no setScaleX/setScaleY (and scaling a redrawn shape is unreliable), so + // we tween a size proxy and redraw every frame instead. + animateSize(w, h, duration = 240, ease = 'Quad.easeOut', onComplete) { + if (this._sizeTween) this._sizeTween.stop(); + const proxy = { w: this._w, h: this._h }; + this._sizeTween = this.scene.tweens.add({ + targets: proxy, + w, + h, + duration, + ease, + onUpdate: () => { + if (!this.scene) return; // destroyed mid-animation + this.redraw(proxy.w, proxy.h); + }, + onComplete: () => { + this._sizeTween = null; + if (!this.scene) return; // destroyed mid-animation + this.redraw(w, h); + if (onComplete) onComplete(); + }, + }); + } +}