B1 Seeded RNG + Daily Challenge - utils/random.js wraps Phaser seedable RND (Between/frac are NOT seedable) - All gameplay spawning (PlatformManager, Platform, Enemy) uses seeded rng - GameScene reads mode/seed in init and seeds the run; daily shows a HUD badge and keeps a per-day best (daily_<YYYYMMDD>); MenuScene DAILY button; GameOver RETRY preserves mode and shows today's best - Verified: same seed -> identical layout, different seed -> different B2 New content - Enemy mev_bot: homing chaser that eases toward the player (unlock >1500) - Platform reorg: phantom, semi-transparent, vanishes shortly after landing (unlock >600); no power-ups on breaking/reorg; SPAWN_RATES + UNLOCK config - Verified spawn distribution at high difficulty includes all new types B3 Settings - SoundManager gains volume (persisted); MenuScene SETTINGS overlay with volume stepper, particle-quality Low/High toggle, two-step reset progress B4 Stats - StatsManager tracks lifetime games/jumps/stomps/blocks/best combo, flushed at game over; MenuScene STATS overlay; hooks in GameScene/ScoreManager B5 Difficulty tuning via UNLOCK thresholds and rebalanced spawn rates Functionally verified in-browser via eval (no console errors, deterministic daily, content spawns, particles emit). Visual screenshot unavailable in the headless preview because the hidden tab pauses Phaser's loop.
325 lines
12 KiB
JavaScript
325 lines
12 KiB
JavaScript
import { Scene } from 'phaser';
|
|
import { sound } from '../managers/SoundManager.js';
|
|
import { AchievementsManager } from '../managers/AchievementsManager.js';
|
|
import { StatsManager } from '../managers/StatsManager.js';
|
|
import { ParticleManager } from '../managers/ParticleManager.js';
|
|
import { createButton } from '../utils/ui.js';
|
|
import { storage, KEYS } from '../utils/storage.js';
|
|
import { todaySeed } from '../utils/random.js';
|
|
|
|
export class MenuScene extends Scene {
|
|
constructor() {
|
|
super({ key: 'MenuScene' });
|
|
}
|
|
|
|
create() {
|
|
const { width, height } = this.scale;
|
|
|
|
this.add.tileSprite(width / 2, height / 2, width, height, 'gridBg');
|
|
|
|
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.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.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' });
|
|
|
|
createButton(this, width / 2, height * 0.585, 'START GAME', () => this.startGame(), {
|
|
width: 280, height: 54, fontSize: '15px',
|
|
});
|
|
|
|
this.add.text(width / 2, height * 0.635, 'ENTER / SPACE / TAP', {
|
|
fontFamily: '"Press Start 2P", monospace',
|
|
fontSize: '9px',
|
|
color: '#888',
|
|
align: 'center',
|
|
}).setOrigin(0.5);
|
|
|
|
this.input.keyboard.on('keydown-ENTER', () => this.startGame());
|
|
this.input.keyboard.on('keydown-SPACE', () => this.startGame());
|
|
|
|
createButton(this, width / 2, height * 0.69, 'DAILY CHALLENGE', () => this.startDaily(), {
|
|
width: 280, height: 46, fontSize: '12px', bgColor: 0x854d0e, hoverColor: 0xa16207, strokeColor: 0xffd700,
|
|
});
|
|
|
|
createButton(this, width / 2 - 75, height * 0.775, 'BOARD', () => this.showLeaderboard(), {
|
|
width: 140, height: 42, fontSize: '11px',
|
|
});
|
|
createButton(this, width / 2 + 75, height * 0.775, 'BADGES', () => this.showAchievements(), {
|
|
width: 140, height: 42, fontSize: '11px',
|
|
});
|
|
createButton(this, width / 2 - 75, height * 0.85, 'STATS', () => this.showStats(), {
|
|
width: 140, height: 42, fontSize: '11px',
|
|
});
|
|
createButton(this, width / 2 + 75, height * 0.85, 'SETTINGS', () => this.showSettings(), {
|
|
width: 140, height: 42, fontSize: '11px',
|
|
});
|
|
|
|
this.createMuteButton();
|
|
|
|
this.add.text(width / 2, height * 0.95, 'Web3 integration coming soon', {
|
|
fontFamily: '"Press Start 2P", monospace',
|
|
fontSize: '9px',
|
|
color: '#444',
|
|
align: 'center',
|
|
}).setOrigin(0.5);
|
|
|
|
this.createAmbientParticles();
|
|
|
|
this.input.once('pointerdown', () => { sound.init(); sound.resume(); });
|
|
this.input.keyboard.once('keydown', () => { sound.init(); sound.resume(); });
|
|
}
|
|
|
|
startGame() {
|
|
sound.init();
|
|
sound.resume();
|
|
sound.click();
|
|
this.scene.start('GameScene', { mode: 'normal' });
|
|
}
|
|
|
|
startDaily() {
|
|
sound.init();
|
|
sound.resume();
|
|
sound.click();
|
|
this.scene.start('GameScene', { mode: 'daily', seed: todaySeed() });
|
|
}
|
|
|
|
createMuteButton() {
|
|
const { width } = this.scale;
|
|
const icon = this.add.text(width - 30, 30, sound.isMuted() ? '🔇' : '🔊', {
|
|
fontSize: '24px',
|
|
}).setOrigin(0.5).setInteractive({ useHandCursor: true });
|
|
|
|
icon.on('pointerdown', () => {
|
|
sound.init();
|
|
const next = !sound.isMuted();
|
|
sound.setMuted(next);
|
|
icon.setText(next ? '🔇' : '🔊');
|
|
if (!next) sound.click();
|
|
});
|
|
}
|
|
|
|
showLeaderboard() {
|
|
const { width, height } = this.scale;
|
|
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.7).setDepth(100);
|
|
const panel = this.add.rectangle(width / 2, height / 2, 400, 440, 0x1a0533).setStrokeStyle(3, 0xa855f7).setDepth(101);
|
|
|
|
const title = this.add.text(width / 2, height * 0.22, 'LEADERBOARD', {
|
|
fontFamily: '"Press Start 2P", monospace',
|
|
fontSize: '18px',
|
|
color: '#d8b4fe',
|
|
}).setOrigin(0.5).setDepth(102);
|
|
|
|
const close = this.add.text(width / 2 + 170, height * 0.22 - 80, 'X', {
|
|
fontFamily: '"Press Start 2P", monospace',
|
|
fontSize: '18px',
|
|
color: '#fff',
|
|
}).setOrigin(0.5).setDepth(102).setInteractive({ useHandCursor: true });
|
|
|
|
const rows = [];
|
|
const mock = [
|
|
{ rank: 1, addr: '0xMonad...Dev', score: 99999 },
|
|
{ rank: 2, addr: '0xAlice...xyz', score: 87500 },
|
|
{ rank: 3, addr: '0xBob...abc', score: 74200 },
|
|
{ rank: 4, addr: '0xYou', score: storage.getInt(KEYS.best, 0) },
|
|
];
|
|
mock.forEach((entry, i) => {
|
|
const y = height * 0.32 + i * 55;
|
|
rows.push({
|
|
rank: this.add.text(width / 2 - 160, y, `#${entry.rank}`, { fontFamily: '"Press Start 2P", monospace', fontSize: '12px', color: '#a855f7' }).setOrigin(0.5).setDepth(102),
|
|
addr: this.add.text(width / 2, y, entry.addr, { fontFamily: '"Press Start 2P", monospace', fontSize: '10px', color: '#fff' }).setOrigin(0.5).setDepth(102),
|
|
score: this.add.text(width / 2 + 150, y, String(entry.score), { fontFamily: '"Press Start 2P", monospace', fontSize: '12px', color: '#d8b4fe' }).setOrigin(0.5).setDepth(102),
|
|
});
|
|
});
|
|
|
|
close.on('pointerdown', () => {
|
|
sound.click();
|
|
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 = storage.getJSON(KEYS.achievements, {}) || {};
|
|
|
|
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());
|
|
});
|
|
}
|
|
|
|
_openModal(titleText, panelH = 480) {
|
|
const { width, height } = this.scale;
|
|
const elements = [];
|
|
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.75).setDepth(100)
|
|
.setInteractive();
|
|
const panel = this.add.rectangle(width / 2, height / 2, 420, panelH, 0x1a0533).setStrokeStyle(3, 0xa855f7).setDepth(101);
|
|
const title = this.add.text(width / 2, height / 2 - panelH / 2 + 34, titleText, {
|
|
fontFamily: '"Press Start 2P", monospace', fontSize: '15px', color: '#d8b4fe',
|
|
}).setOrigin(0.5).setDepth(102);
|
|
const close = this.add.text(width / 2 + 185, height / 2 - panelH / 2 + 22, 'X', {
|
|
fontFamily: '"Press Start 2P", monospace', fontSize: '18px', color: '#fff',
|
|
}).setOrigin(0.5).setDepth(102).setInteractive({ useHandCursor: true });
|
|
elements.push(overlay, panel, title, close);
|
|
|
|
const api = {
|
|
elements,
|
|
add: (obj) => { obj.setDepth(102); elements.push(obj); return obj; },
|
|
close: () => { sound.click(); elements.forEach((e) => e.destroy()); },
|
|
};
|
|
close.on('pointerdown', api.close);
|
|
return api;
|
|
}
|
|
|
|
showStats() {
|
|
const { width, height } = this.scale;
|
|
const s = StatsManager.load();
|
|
const m = this._openModal('STATS', 420);
|
|
const rows = [
|
|
['Games played', s.gamesPlayed],
|
|
['Total jumps', s.totalJumps],
|
|
['Enemies stomped', s.totalStomps],
|
|
['Blocks climbed', s.totalBlocks],
|
|
['Best combo', `x${(s.bestCombo || 1).toFixed(1)}`],
|
|
['Best score', storage.getInt(KEYS.best, 0)],
|
|
];
|
|
rows.forEach((row, i) => {
|
|
const y = height / 2 - 100 + i * 42;
|
|
m.add(this.add.text(width / 2 - 170, y, row[0], {
|
|
fontFamily: '"Press Start 2P", monospace', fontSize: '10px', color: '#d8b4fe',
|
|
}).setOrigin(0, 0.5));
|
|
m.add(this.add.text(width / 2 + 170, y, String(row[1]), {
|
|
fontFamily: '"Press Start 2P", monospace', fontSize: '11px', color: '#ffd700',
|
|
}).setOrigin(1, 0.5));
|
|
});
|
|
}
|
|
|
|
showSettings() {
|
|
const { width, height } = this.scale;
|
|
const m = this._openModal('SETTINGS', 460);
|
|
const cx = width / 2;
|
|
let baseY = height / 2 - 130;
|
|
|
|
// Volume stepper
|
|
m.add(this.add.text(cx, baseY, 'VOLUME', {
|
|
fontFamily: '"Press Start 2P", monospace', fontSize: '11px', color: '#d8b4fe',
|
|
}).setOrigin(0.5));
|
|
const volText = this.add.text(cx, baseY + 34, `${Math.round(sound.getVolume() * 100)}%`, {
|
|
fontFamily: '"Press Start 2P", monospace', fontSize: '13px', color: '#ffd700',
|
|
}).setOrigin(0.5);
|
|
m.add(volText);
|
|
const stepVol = (delta) => {
|
|
sound.init();
|
|
sound.setVolume(Math.round((sound.getVolume() + delta) * 10) / 10);
|
|
if (sound.isMuted() && sound.getVolume() > 0) sound.setMuted(false);
|
|
volText.setText(`${Math.round(sound.getVolume() * 100)}%`);
|
|
sound.click();
|
|
};
|
|
m.add(createButton(this, cx - 90, baseY + 34, '-', () => stepVol(-0.1), { width: 44, height: 36, fontSize: '14px' }).bg);
|
|
m.add(createButton(this, cx + 90, baseY + 34, '+', () => stepVol(0.1), { width: 44, height: 36, fontSize: '14px' }).bg);
|
|
|
|
// Particle quality toggle
|
|
baseY += 96;
|
|
m.add(this.add.text(cx, baseY, 'PARTICLE QUALITY', {
|
|
fontFamily: '"Press Start 2P", monospace', fontSize: '11px', color: '#d8b4fe',
|
|
}).setOrigin(0.5));
|
|
const currentQ = () => storage.getItem(KEYS.particleQuality, ParticleManager.resolveQuality(this));
|
|
const qBtn = createButton(this, cx, baseY + 34, currentQ().toUpperCase(), () => {
|
|
const next = currentQ() === 'low' ? 'high' : 'low';
|
|
storage.setItem(KEYS.particleQuality, next);
|
|
qBtn.label.setText(next.toUpperCase());
|
|
sound.click();
|
|
}, { width: 160, height: 38, fontSize: '12px' });
|
|
m.add(qBtn.bg); m.add(qBtn.label);
|
|
|
|
// Reset progress (two-step confirm)
|
|
baseY += 96;
|
|
let armed = false;
|
|
const resetBtn = createButton(this, cx, baseY + 10, 'RESET PROGRESS', () => {
|
|
if (!armed) {
|
|
armed = true;
|
|
resetBtn.label.setText('TAP AGAIN!');
|
|
resetBtn.bg.setFillStyle(0x7f1d1d);
|
|
return;
|
|
}
|
|
StatsManager.reset();
|
|
storage.removeItem(KEYS.best);
|
|
storage.removeItem(KEYS.achievements);
|
|
resetBtn.label.setText('DONE');
|
|
sound.click();
|
|
}, { width: 240, height: 42, fontSize: '12px', bgColor: 0x581c87, hoverColor: 0x7e22ce });
|
|
m.add(resetBtn.bg); m.add(resetBtn.label);
|
|
}
|
|
|
|
createAmbientParticles() {
|
|
const { width, height } = this.scale;
|
|
for (let i = 0; i < 40; i++) {
|
|
const s = this.add.image(Phaser.Math.Between(0, width), Phaser.Math.Between(0, height), 'star');
|
|
s.setAlpha(Phaser.Math.FloatBetween(0.15, 0.7));
|
|
this.tweens.add({
|
|
targets: s,
|
|
y: s.y - Phaser.Math.Between(50, 250),
|
|
alpha: 0,
|
|
duration: Phaser.Math.Between(2000, 6000),
|
|
repeat: -1,
|
|
delay: Phaser.Math.Between(0, 3000),
|
|
});
|
|
}
|
|
}
|
|
}
|