From d509f1df4a08c18aac4612605da22dc8528c2f54 Mon Sep 17 00:00:00 2001 From: AnRil Date: Sat, 23 May 2026 17:00:56 +0700 Subject: [PATCH] =?UTF-8?q?Sprint=203:=20architecture=20=E2=80=94=20UI=20u?= =?UTF-8?q?tility,=20achievements=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/scenes/GameOverScene.js | 27 +------- src/scenes/MenuScene.js | 122 +++++++++++++++++++++--------------- src/utils/ui.js | 51 +++++++++++++++ 3 files changed, 127 insertions(+), 73 deletions(-) create mode 100644 src/utils/ui.js diff --git a/src/scenes/GameOverScene.js b/src/scenes/GameOverScene.js index 6b10a09..6d22737 100644 --- a/src/scenes/GameOverScene.js +++ b/src/scenes/GameOverScene.js @@ -1,5 +1,6 @@ import { Scene } from 'phaser'; import { sound } from '../managers/SoundManager.js'; +import { createButton } from '../utils/ui.js'; export class GameOverScene extends Scene { constructor() { @@ -48,15 +49,8 @@ export class GameOverScene extends Scene { color: '#a855f7', }).setOrigin(0.5); - this.createButton(width / 2, height * 0.72, 'RETRY', () => { - sound.click(); - this.scene.start('GameScene'); - }); - - this.createButton(width / 2, height * 0.82, 'MAIN MENU', () => { - sound.click(); - this.scene.start('MenuScene'); - }); + createButton(this, width / 2, height * 0.72, 'RETRY', () => this.scene.start('GameScene')); + createButton(this, width / 2, height * 0.82, 'MAIN MENU', () => this.scene.start('MenuScene')); this.add.text(width / 2, height * 0.92, 'ON-CHAIN SUBMIT — COMING SOON', { fontFamily: '"Press Start 2P", monospace', @@ -74,19 +68,4 @@ export class GameOverScene extends Scene { 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 }; - } } diff --git a/src/scenes/MenuScene.js b/src/scenes/MenuScene.js index 6c3a7cc..e0c5fd5 100644 --- a/src/scenes/MenuScene.js +++ b/src/scenes/MenuScene.js @@ -1,5 +1,7 @@ import { Scene } from 'phaser'; import { sound } from '../managers/SoundManager.js'; +import { AchievementsManager } from '../managers/AchievementsManager.js'; +import { createButton } from '../utils/ui.js'; export class MenuScene extends Scene { constructor() { @@ -11,27 +13,29 @@ export class MenuScene extends Scene { 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', fontSize: '38px', color: '#d8b4fe', align: 'center', }).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', fontSize: '14px', color: '#a855f7', align: 'center', }).setOrigin(0.5); - const preview = this.add.image(width / 2, height * 0.48, 'player_idle').setScale(0.55); - this.tweens.add({ targets: preview, y: height * 0.48 - 15, duration: 1400, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); + const preview = this.add.image(width / 2, height * 0.44, 'player_idle').setScale(0.55); + 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.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', fontSize: '10px', color: '#888', @@ -41,7 +45,12 @@ export class MenuScene extends Scene { this.input.keyboard.on('keydown-ENTER', () => 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(); @@ -54,14 +63,8 @@ export class MenuScene extends Scene { this.createAmbientParticles(); - this.input.once('pointerdown', () => { - sound.init(); - sound.resume(); - }); - this.input.keyboard.once('keydown', () => { - sound.init(); - sound.resume(); - }); + this.input.once('pointerdown', () => { sound.init(); sound.resume(); }); + this.input.keyboard.once('keydown', () => { sound.init(); sound.resume(); }); } startGame() { @@ -71,38 +74,9 @@ export class MenuScene extends Scene { 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() { const { width } = this.scale; - const x = width - 30; - const y = 30; - const icon = this.add.text(x, y, sound.isMuted() ? '🔇' : '🔊', { + const icon = this.add.text(width - 30, 30, sound.isMuted() ? '🔇' : '🔊', { fontSize: '24px', }).setOrigin(0.5).setInteractive({ useHandCursor: true }); @@ -150,14 +124,64 @@ export class MenuScene extends Scene { close.on('pointerdown', () => { sound.click(); - overlay.destroy(); - panel.destroy(); - title.destroy(); - close.destroy(); + overlay.destroy(); panel.destroy(); title.destroy(); close.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() { const { width, height } = this.scale; for (let i = 0; i < 40; i++) { diff --git a/src/utils/ui.js b/src/utils/ui.js new file mode 100644 index 0000000..971489b --- /dev/null +++ b/src/utils/ui.js @@ -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); +}