Sprint 3: architecture — UI utility, achievements menu

- Extract createButton/pixelText helpers to src/utils/ui.js with
  sensible defaults and per-call option overrides
- MenuScene now shows BADGES button opening the achievement panel
  (10 entries, count of unlocked, star icons for completed)
- GameOverScene buttons migrated to shared utility, removing duplicate
  hover/click handlers
- Smaller LEADERBOARD button to make room for BADGES alongside
This commit is contained in:
2026-05-23 17:00:56 +07:00
parent 57f9e2f282
commit d509f1df4a
3 changed files with 127 additions and 73 deletions

View File

@@ -1,5 +1,6 @@
import { Scene } from 'phaser'; import { Scene } from 'phaser';
import { sound } from '../managers/SoundManager.js'; import { sound } from '../managers/SoundManager.js';
import { createButton } from '../utils/ui.js';
export class GameOverScene extends Scene { export class GameOverScene extends Scene {
constructor() { constructor() {
@@ -48,15 +49,8 @@ export class GameOverScene extends Scene {
color: '#a855f7', color: '#a855f7',
}).setOrigin(0.5); }).setOrigin(0.5);
this.createButton(width / 2, height * 0.72, 'RETRY', () => { createButton(this, width / 2, height * 0.72, 'RETRY', () => this.scene.start('GameScene'));
sound.click(); createButton(this, width / 2, height * 0.82, 'MAIN MENU', () => this.scene.start('MenuScene'));
this.scene.start('GameScene');
});
this.createButton(width / 2, height * 0.82, 'MAIN MENU', () => {
sound.click();
this.scene.start('MenuScene');
});
this.add.text(width / 2, height * 0.92, 'ON-CHAIN SUBMIT — COMING SOON', { this.add.text(width / 2, height * 0.92, 'ON-CHAIN SUBMIT — COMING SOON', {
fontFamily: '"Press Start 2P", monospace', fontFamily: '"Press Start 2P", monospace',
@@ -74,19 +68,4 @@ export class GameOverScene extends Scene {
this.scene.start('GameScene'); this.scene.start('GameScene');
}); });
} }
createButton(x, y, text, callback) {
const bg = this.add.rectangle(x, y, 260, 48, 0x581c87)
.setStrokeStyle(2, 0xa855f7)
.setInteractive({ useHandCursor: true });
const label = this.add.text(x, y, text, {
fontFamily: '"Press Start 2P", monospace',
fontSize: '13px',
color: '#ffffff',
}).setOrigin(0.5);
bg.on('pointerover', () => bg.setFillStyle(0x7e22ce));
bg.on('pointerout', () => bg.setFillStyle(0x581c87));
bg.on('pointerdown', callback);
return { bg, label };
}
} }

View File

@@ -1,5 +1,7 @@
import { Scene } from 'phaser'; import { Scene } from 'phaser';
import { sound } from '../managers/SoundManager.js'; import { sound } from '../managers/SoundManager.js';
import { AchievementsManager } from '../managers/AchievementsManager.js';
import { createButton } from '../utils/ui.js';
export class MenuScene extends Scene { export class MenuScene extends Scene {
constructor() { constructor() {
@@ -11,27 +13,29 @@ export class MenuScene extends Scene {
this.add.tileSprite(width / 2, height / 2, width, height, 'gridBg'); this.add.tileSprite(width / 2, height / 2, width, height, 'gridBg');
this.add.text(width / 2, height * 0.18, 'NADDIE JUMP', { this.add.text(width / 2, height * 0.16, 'NADDIE JUMP', {
fontFamily: '"Press Start 2P", monospace', fontFamily: '"Press Start 2P", monospace',
fontSize: '38px', fontSize: '38px',
color: '#d8b4fe', color: '#d8b4fe',
align: 'center', align: 'center',
}).setOrigin(0.5).setShadow(4, 4, '#581c87', 0, false, true); }).setOrigin(0.5).setShadow(4, 4, '#581c87', 0, false, true);
this.add.text(width / 2, height * 0.27, 'MONAD EDITION', { this.add.text(width / 2, height * 0.25, 'MONAD EDITION', {
fontFamily: '"Press Start 2P", monospace', fontFamily: '"Press Start 2P", monospace',
fontSize: '14px', fontSize: '14px',
color: '#a855f7', color: '#a855f7',
align: 'center', align: 'center',
}).setOrigin(0.5); }).setOrigin(0.5);
const preview = this.add.image(width / 2, height * 0.48, 'player_idle').setScale(0.55); const preview = this.add.image(width / 2, height * 0.44, 'player_idle').setScale(0.55);
this.tweens.add({ targets: preview, y: height * 0.48 - 15, duration: 1400, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); this.tweens.add({ targets: preview, y: height * 0.44 - 15, duration: 1400, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' });
this.tweens.add({ targets: preview, angle: 5, duration: 2000, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); this.tweens.add({ targets: preview, angle: 5, duration: 2000, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' });
this.createButton(width / 2, height * 0.68, 'START GAME', () => this.startGame()); createButton(this, width / 2, height * 0.62, 'START GAME', () => this.startGame(), {
width: 280, height: 56, fontSize: '15px',
});
this.add.text(width / 2, height * 0.74, 'ENTER / SPACE / TAP', { this.add.text(width / 2, height * 0.68, 'ENTER / SPACE / TAP', {
fontFamily: '"Press Start 2P", monospace', fontFamily: '"Press Start 2P", monospace',
fontSize: '10px', fontSize: '10px',
color: '#888', color: '#888',
@@ -41,7 +45,12 @@ export class MenuScene extends Scene {
this.input.keyboard.on('keydown-ENTER', () => this.startGame()); this.input.keyboard.on('keydown-ENTER', () => this.startGame());
this.input.keyboard.on('keydown-SPACE', () => this.startGame()); this.input.keyboard.on('keydown-SPACE', () => this.startGame());
this.createButton(width / 2, height * 0.82, 'LEADERBOARD', () => this.showLeaderboard()); createButton(this, width / 2 - 75, height * 0.78, 'BOARD', () => this.showLeaderboard(), {
width: 140, height: 44, fontSize: '11px',
});
createButton(this, width / 2 + 75, height * 0.78, 'BADGES', () => this.showAchievements(), {
width: 140, height: 44, fontSize: '11px',
});
this.createMuteButton(); this.createMuteButton();
@@ -54,14 +63,8 @@ export class MenuScene extends Scene {
this.createAmbientParticles(); this.createAmbientParticles();
this.input.once('pointerdown', () => { this.input.once('pointerdown', () => { sound.init(); sound.resume(); });
sound.init(); this.input.keyboard.once('keydown', () => { sound.init(); sound.resume(); });
sound.resume();
});
this.input.keyboard.once('keydown', () => {
sound.init();
sound.resume();
});
} }
startGame() { startGame() {
@@ -71,38 +74,9 @@ export class MenuScene extends Scene {
this.scene.start('GameScene'); this.scene.start('GameScene');
} }
createButton(x, y, text, callback) {
const bg = this.add.rectangle(x, y, 280, 56, 0x581c87)
.setStrokeStyle(3, 0xa855f7)
.setInteractive({ useHandCursor: true });
const label = this.add.text(x, y, text, {
fontFamily: '"Press Start 2P", monospace',
fontSize: '15px',
color: '#ffffff',
}).setOrigin(0.5);
bg.on('pointerover', () => {
bg.setFillStyle(0x7e22ce);
this.tweens.add({ targets: [bg, label], scaleX: 1.05, scaleY: 1.05, duration: 100 });
});
bg.on('pointerout', () => {
bg.setFillStyle(0x581c87);
this.tweens.add({ targets: [bg, label], scaleX: 1, scaleY: 1, duration: 100 });
});
bg.on('pointerdown', () => {
sound.click();
callback();
});
return { bg, label };
}
createMuteButton() { createMuteButton() {
const { width } = this.scale; const { width } = this.scale;
const x = width - 30; const icon = this.add.text(width - 30, 30, sound.isMuted() ? '🔇' : '🔊', {
const y = 30;
const icon = this.add.text(x, y, sound.isMuted() ? '🔇' : '🔊', {
fontSize: '24px', fontSize: '24px',
}).setOrigin(0.5).setInteractive({ useHandCursor: true }); }).setOrigin(0.5).setInteractive({ useHandCursor: true });
@@ -150,14 +124,64 @@ export class MenuScene extends Scene {
close.on('pointerdown', () => { close.on('pointerdown', () => {
sound.click(); sound.click();
overlay.destroy(); overlay.destroy(); panel.destroy(); title.destroy(); close.destroy();
panel.destroy();
title.destroy();
close.destroy();
rows.forEach(r => { r.rank.destroy(); r.addr.destroy(); r.score.destroy(); }); rows.forEach(r => { r.rank.destroy(); r.addr.destroy(); r.score.destroy(); });
}); });
} }
showAchievements() {
const { width, height } = this.scale;
const all = AchievementsManager.getAll();
const unlocked = JSON.parse(localStorage.getItem('naddie_achievements_v1') || '{}');
const elements = [];
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.75).setDepth(100);
const panel = this.add.rectangle(width / 2, height / 2, 420, 700, 0x1a0533).setStrokeStyle(3, 0xa855f7).setDepth(101);
const total = all.length;
const got = Object.keys(unlocked).length;
const title = this.add.text(width / 2, height * 0.12, `ACHIEVEMENTS ${got}/${total}`, {
fontFamily: '"Press Start 2P", monospace',
fontSize: '14px',
color: '#d8b4fe',
}).setOrigin(0.5).setDepth(102);
elements.push(title);
const close = this.add.text(width / 2 + 190, height * 0.12 - 5, 'X', {
fontFamily: '"Press Start 2P", monospace',
fontSize: '18px',
color: '#fff',
}).setOrigin(0.5).setDepth(102).setInteractive({ useHandCursor: true });
elements.push(close);
all.forEach((def, i) => {
const y = height * 0.18 + i * 55;
const isGot = !!unlocked[def.id];
const icon = this.add.text(width / 2 - 180, y, isGot ? '★' : '☆', {
fontFamily: '"Press Start 2P", monospace',
fontSize: '18px',
color: isGot ? '#ffd700' : '#555',
}).setOrigin(0.5).setDepth(102);
const nameText = this.add.text(width / 2 - 150, y - 8, def.title, {
fontFamily: '"Press Start 2P", monospace',
fontSize: '10px',
color: isGot ? '#ffd700' : '#888',
}).setOrigin(0, 0.5).setDepth(102);
const descText = this.add.text(width / 2 - 150, y + 10, def.desc, {
fontFamily: '"Press Start 2P", monospace',
fontSize: '8px',
color: '#d8b4fe',
}).setOrigin(0, 0.5).setDepth(102);
elements.push(icon, nameText, descText);
});
close.on('pointerdown', () => {
sound.click();
overlay.destroy(); panel.destroy();
elements.forEach((e) => e.destroy());
});
}
createAmbientParticles() { createAmbientParticles() {
const { width, height } = this.scale; const { width, height } = this.scale;
for (let i = 0; i < 40; i++) { for (let i = 0; i < 40; i++) {

51
src/utils/ui.js Normal file
View File

@@ -0,0 +1,51 @@
import { sound } from '../managers/SoundManager.js';
const DEFAULTS = {
width: 260,
height: 48,
fontSize: '13px',
bgColor: 0x581c87,
hoverColor: 0x7e22ce,
strokeColor: 0xa855f7,
strokeWidth: 2,
textColor: '#ffffff',
hoverScale: 1.04,
playClick: true,
};
export function createButton(scene, x, y, text, callback, options = {}) {
const opt = { ...DEFAULTS, ...options };
const bg = scene.add.rectangle(x, y, opt.width, opt.height, opt.bgColor)
.setStrokeStyle(opt.strokeWidth, opt.strokeColor)
.setInteractive({ useHandCursor: true });
const label = scene.add.text(x, y, text, {
fontFamily: '"Press Start 2P", monospace',
fontSize: opt.fontSize,
color: opt.textColor,
}).setOrigin(0.5);
bg.on('pointerover', () => {
bg.setFillStyle(opt.hoverColor);
scene.tweens.add({ targets: [bg, label], scaleX: opt.hoverScale, scaleY: opt.hoverScale, duration: 100 });
});
bg.on('pointerout', () => {
bg.setFillStyle(opt.bgColor);
scene.tweens.add({ targets: [bg, label], scaleX: 1, scaleY: 1, duration: 100 });
});
bg.on('pointerdown', () => {
if (opt.playClick) sound.click();
callback();
});
return { bg, label, destroy: () => { bg.destroy(); label.destroy(); } };
}
export function pixelText(scene, x, y, text, size = '14px', color = '#ffffff') {
return scene.add.text(x, y, text, {
fontFamily: '"Press Start 2P", monospace',
fontSize: size,
color,
}).setOrigin(0.5);
}