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:
@@ -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 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
51
src/utils/ui.js
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user