Polish #8

Merged
brianfertig merged 5 commits from Polish into main 2026-09-01 04:46:30 +00:00
3 changed files with 82 additions and 3 deletions
Showing only changes of commit 74cd4df85b - Show all commits

View File

@ -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);

View File

@ -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;

62
src/ui/Plaque.js Normal file
View File

@ -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();
},
});
}
}