Sprint 1: polish — sound, pause, tutorial, touch UI
- SoundManager via Web Audio API (no asset files needed) with procedural SFX for jump, spring, powerup, stomp, break, death, milestone, new-best - Sound persists mute state in localStorage; mute button on Menu and Game - Pause system: ESC key or onscreen pause button, modal overlay with Resume and Main Menu options, physics correctly paused/resumed - First-run tutorial overlay explaining controls and platform types, dismissed and remembered via localStorage flag - Touch indicator hints fade after 3.5s on touch devices only - Menu start triggers AudioContext initialization (browser autoplay rules) - GameOverScene supports ENTER/SPACE shortcut for retry, NEW BEST text now pulses, sounds fire on each transition
This commit is contained in:
131
src/managers/SoundManager.js
Normal file
131
src/managers/SoundManager.js
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
class SoundManager {
|
||||||
|
constructor() {
|
||||||
|
this.ctx = null;
|
||||||
|
this.master = null;
|
||||||
|
this.muted = false;
|
||||||
|
this.musicNodes = null;
|
||||||
|
|
||||||
|
const raw = localStorage.getItem('naddie_muted');
|
||||||
|
this.muted = raw === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.ctx) return;
|
||||||
|
const AC = window.AudioContext || window.webkitAudioContext;
|
||||||
|
if (!AC) return;
|
||||||
|
this.ctx = new AC();
|
||||||
|
this.master = this.ctx.createGain();
|
||||||
|
this.master.gain.value = 0.4;
|
||||||
|
this.master.connect(this.ctx.destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
resume() {
|
||||||
|
if (this.ctx && this.ctx.state === 'suspended') {
|
||||||
|
this.ctx.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMuted(value) {
|
||||||
|
this.muted = value;
|
||||||
|
localStorage.setItem('naddie_muted', value ? '1' : '0');
|
||||||
|
if (this.master) {
|
||||||
|
this.master.gain.value = value ? 0 : 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isMuted() {
|
||||||
|
return this.muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
_beep(freq, duration, type = 'square', volume = 0.3) {
|
||||||
|
if (!this.ctx || this.muted) return;
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.type = type;
|
||||||
|
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
|
||||||
|
gain.gain.setValueAtTime(volume, this.ctx.currentTime);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.master);
|
||||||
|
osc.start();
|
||||||
|
osc.stop(this.ctx.currentTime + duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sweep(freqStart, freqEnd, duration, type = 'square', volume = 0.3) {
|
||||||
|
if (!this.ctx || this.muted) return;
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.type = type;
|
||||||
|
osc.frequency.setValueAtTime(freqStart, this.ctx.currentTime);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(Math.max(1, freqEnd), this.ctx.currentTime + duration);
|
||||||
|
gain.gain.setValueAtTime(volume, this.ctx.currentTime);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.master);
|
||||||
|
osc.start();
|
||||||
|
osc.stop(this.ctx.currentTime + duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
_noise(duration, volume = 0.15) {
|
||||||
|
if (!this.ctx || this.muted) return;
|
||||||
|
const bufferSize = this.ctx.sampleRate * duration;
|
||||||
|
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
|
||||||
|
const data = buffer.getChannelData(0);
|
||||||
|
for (let i = 0; i < bufferSize; i++) {
|
||||||
|
data[i] = (Math.random() * 2 - 1) * (1 - i / bufferSize);
|
||||||
|
}
|
||||||
|
const src = this.ctx.createBufferSource();
|
||||||
|
src.buffer = buffer;
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
gain.gain.value = volume;
|
||||||
|
src.connect(gain);
|
||||||
|
gain.connect(this.master);
|
||||||
|
src.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
jump() {
|
||||||
|
this._sweep(320, 540, 0.1, 'square', 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
land() {
|
||||||
|
this._beep(180, 0.06, 'triangle', 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
spring() {
|
||||||
|
this._sweep(280, 1000, 0.18, 'square', 0.22);
|
||||||
|
setTimeout(() => this._sweep(800, 1300, 0.12, 'triangle', 0.15), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
powerup() {
|
||||||
|
[440, 554, 659, 880].forEach((f, i) => setTimeout(() => this._beep(f, 0.13, 'triangle', 0.20), i * 55));
|
||||||
|
}
|
||||||
|
|
||||||
|
stomp() {
|
||||||
|
this._sweep(420, 100, 0.16, 'sawtooth', 0.22);
|
||||||
|
this._noise(0.08, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
break() {
|
||||||
|
this._noise(0.15, 0.12);
|
||||||
|
this._sweep(200, 60, 0.2, 'sawtooth', 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
death() {
|
||||||
|
this._sweep(280, 50, 0.5, 'sawtooth', 0.28);
|
||||||
|
setTimeout(() => this._noise(0.3, 0.15), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
milestone() {
|
||||||
|
[523, 659, 783, 1046].forEach((f, i) => setTimeout(() => this._beep(f, 0.22, 'triangle', 0.25), i * 90));
|
||||||
|
}
|
||||||
|
|
||||||
|
click() {
|
||||||
|
this._beep(660, 0.05, 'square', 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
newBest() {
|
||||||
|
[523, 659, 783, 1046, 1318].forEach((f, i) => setTimeout(() => this._beep(f, 0.18, 'triangle', 0.25), i * 80));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sound = new SoundManager();
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Scene } from 'phaser';
|
import { Scene } from 'phaser';
|
||||||
|
import { sound } from '../managers/SoundManager.js';
|
||||||
|
|
||||||
export class GameOverScene extends Scene {
|
export class GameOverScene extends Scene {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -16,19 +17,9 @@ export class GameOverScene extends Scene {
|
|||||||
|
|
||||||
this.add.tileSprite(width / 2, height / 2, width, height, 'gridBg');
|
this.add.tileSprite(width / 2, height / 2, width, height, 'gridBg');
|
||||||
|
|
||||||
// Dead Naddie image
|
const naddie = this.add.image(width / 2, height * 0.22, 'player_dead').setScale(0.32);
|
||||||
const naddie = this.add.image(width / 2, height * 0.22, 'player_dead')
|
this.tweens.add({ targets: naddie, angle: -10, duration: 2000, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' });
|
||||||
.setScale(0.32);
|
|
||||||
this.tweens.add({
|
|
||||||
targets: naddie,
|
|
||||||
angle: -10,
|
|
||||||
duration: 2000,
|
|
||||||
yoyo: true,
|
|
||||||
repeat: -1,
|
|
||||||
ease: 'Sine.easeInOut',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Game Over text
|
|
||||||
this.add.text(width / 2, height * 0.38, 'GAME OVER', {
|
this.add.text(width / 2, height * 0.38, 'GAME OVER', {
|
||||||
fontFamily: '"Press Start 2P", monospace',
|
fontFamily: '"Press Start 2P", monospace',
|
||||||
fontSize: '32px',
|
fontSize: '32px',
|
||||||
@@ -37,11 +28,12 @@ export class GameOverScene extends Scene {
|
|||||||
}).setOrigin(0.5).setShadow(3, 3, '#7f1d1d', 0, false, true);
|
}).setOrigin(0.5).setShadow(3, 3, '#7f1d1d', 0, false, true);
|
||||||
|
|
||||||
if (this.isNewBest) {
|
if (this.isNewBest) {
|
||||||
this.add.text(width / 2, height * 0.46, 'NEW BEST!', {
|
const best = this.add.text(width / 2, height * 0.46, 'NEW BEST!', {
|
||||||
fontFamily: '"Press Start 2P", monospace',
|
fontFamily: '"Press Start 2P", monospace',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
this.tweens.add({ targets: best, scale: 1.15, duration: 600, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.add.text(width / 2, height * 0.54, `Block Height: ${this.blockHeight}`, {
|
this.add.text(width / 2, height * 0.54, `Block Height: ${this.blockHeight}`, {
|
||||||
@@ -57,10 +49,12 @@ export class GameOverScene extends Scene {
|
|||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
this.createButton(width / 2, height * 0.72, 'RETRY', () => {
|
this.createButton(width / 2, height * 0.72, 'RETRY', () => {
|
||||||
|
sound.click();
|
||||||
this.scene.start('GameScene');
|
this.scene.start('GameScene');
|
||||||
});
|
});
|
||||||
|
|
||||||
this.createButton(width / 2, height * 0.82, 'MAIN MENU', () => {
|
this.createButton(width / 2, height * 0.82, 'MAIN MENU', () => {
|
||||||
|
sound.click();
|
||||||
this.scene.start('MenuScene');
|
this.scene.start('MenuScene');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,6 +64,15 @@ export class GameOverScene extends Scene {
|
|||||||
color: '#666',
|
color: '#666',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
this.input.keyboard.once('keydown-ENTER', () => {
|
||||||
|
sound.click();
|
||||||
|
this.scene.start('GameScene');
|
||||||
|
});
|
||||||
|
this.input.keyboard.once('keydown-SPACE', () => {
|
||||||
|
sound.click();
|
||||||
|
this.scene.start('GameScene');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createButton(x, y, text, callback) {
|
createButton(x, y, text, callback) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Player } from '../entities/Player.js';
|
|||||||
import { Platform } from '../entities/Platform.js';
|
import { Platform } from '../entities/Platform.js';
|
||||||
import { PlatformManager } from '../managers/PlatformManager.js';
|
import { PlatformManager } from '../managers/PlatformManager.js';
|
||||||
import { ScoreManager } from '../managers/ScoreManager.js';
|
import { ScoreManager } from '../managers/ScoreManager.js';
|
||||||
|
import { sound } from '../managers/SoundManager.js';
|
||||||
import { GAME_WIDTH, GAME_HEIGHT, SCORE, PHYSICS } from '../config/game.config.js';
|
import { GAME_WIDTH, GAME_HEIGHT, SCORE, PHYSICS } from '../config/game.config.js';
|
||||||
|
|
||||||
export class GameScene extends Scene {
|
export class GameScene extends Scene {
|
||||||
@@ -11,6 +12,9 @@ export class GameScene extends Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
sound.init();
|
||||||
|
sound.resume();
|
||||||
|
|
||||||
this.bg = this.add.tileSprite(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 'gridBg')
|
this.bg = this.add.tileSprite(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 'gridBg')
|
||||||
.setScrollFactor(0)
|
.setScrollFactor(0)
|
||||||
.setDepth(-10);
|
.setDepth(-10);
|
||||||
@@ -19,6 +23,7 @@ export class GameScene extends Scene {
|
|||||||
|
|
||||||
this.cursors = this.input.keyboard.createCursorKeys();
|
this.cursors = this.input.keyboard.createCursorKeys();
|
||||||
this.wasd = this.input.keyboard.addKeys({ up: 'W', down: 'S', left: 'A', right: 'D' });
|
this.wasd = this.input.keyboard.addKeys({ up: 'W', down: 'S', left: 'A', right: 'D' });
|
||||||
|
this.escKey = this.input.keyboard.addKey('ESC');
|
||||||
this.touchLeft = false;
|
this.touchLeft = false;
|
||||||
this.touchRight = false;
|
this.touchRight = false;
|
||||||
this.setupTouchControls();
|
this.setupTouchControls();
|
||||||
@@ -52,12 +57,23 @@ export class GameScene extends Scene {
|
|||||||
this.cameras.main.startFollow(this.player, true, 0, 0.05, 0, 180);
|
this.cameras.main.startFollow(this.player, true, 0, 0.05, 0, 180);
|
||||||
|
|
||||||
this.isGameOver = false;
|
this.isGameOver = false;
|
||||||
|
this.isPaused = false;
|
||||||
this.difficultyLevel = 0;
|
this.difficultyLevel = 0;
|
||||||
this.minScrollY = this.cameras.main.scrollY;
|
this.minScrollY = this.cameras.main.scrollY;
|
||||||
|
|
||||||
|
this.onMilestone = () => sound.milestone();
|
||||||
|
|
||||||
|
this.createPauseUI();
|
||||||
|
this.createMuteButton();
|
||||||
|
|
||||||
|
this.escKey.on('down', () => this.togglePause());
|
||||||
|
|
||||||
|
this.maybeShowTutorial();
|
||||||
|
this.showTouchIndicators();
|
||||||
}
|
}
|
||||||
|
|
||||||
update(time, delta) {
|
update(time, delta) {
|
||||||
if (this.isGameOver) return;
|
if (this.isGameOver || this.isPaused) return;
|
||||||
|
|
||||||
this.player.update(this.cursors, this.wasd, this.touchLeft, this.touchRight, time, delta);
|
this.player.update(this.cursors, this.wasd, this.touchLeft, this.touchRight, time, delta);
|
||||||
|
|
||||||
@@ -130,9 +146,12 @@ export class GameScene extends Scene {
|
|||||||
if (platform && typeof platform.onPlayerLand === 'function') {
|
if (platform && typeof platform.onPlayerLand === 'function') {
|
||||||
platform.onPlayerLand(player);
|
platform.onPlayerLand(player);
|
||||||
this.scoreManager.onLand(platform.y, platform.platformType || 'stable');
|
this.scoreManager.onLand(platform.y, platform.platformType || 'stable');
|
||||||
|
if (platform.platformType === 'breaking') sound.break();
|
||||||
}
|
}
|
||||||
this.lastJumpY = player.y;
|
this.lastJumpY = player.y;
|
||||||
player.jump();
|
if (player.jump()) {
|
||||||
|
sound.jump();
|
||||||
|
}
|
||||||
this.createJumpParticles(player.x, player.y + player.displayHeight / 2 + 3);
|
this.createJumpParticles(player.x, player.y + player.displayHeight / 2 + 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,10 +160,13 @@ export class GameScene extends Scene {
|
|||||||
if (powerup && typeof powerup.onPlayerTouch === 'function') {
|
if (powerup && typeof powerup.onPlayerTouch === 'function') {
|
||||||
const px = powerup.x;
|
const px = powerup.x;
|
||||||
const py = powerup.y;
|
const py = powerup.y;
|
||||||
|
const isSpring = powerup.constructor.name === 'Spring';
|
||||||
const consumed = powerup.onPlayerTouch(player);
|
const consumed = powerup.onPlayerTouch(player);
|
||||||
if (consumed) {
|
if (consumed) {
|
||||||
this.createPowerupParticles(px, py);
|
this.createPowerupParticles(px, py);
|
||||||
this.flashScreen();
|
this.flashScreen();
|
||||||
|
if (isSpring) sound.spring();
|
||||||
|
else sound.powerup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,18 +176,21 @@ export class GameScene extends Scene {
|
|||||||
if (player.state === 'rocket') {
|
if (player.state === 'rocket') {
|
||||||
this.createExplosion(enemy.x, enemy.y);
|
this.createExplosion(enemy.x, enemy.y);
|
||||||
enemy.destroy();
|
enemy.destroy();
|
||||||
|
sound.stomp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (player.state === 'propeller') {
|
if (player.state === 'propeller') {
|
||||||
player.endPowerUp();
|
player.endPowerUp();
|
||||||
this.createExplosion(enemy.x, enemy.y);
|
this.createExplosion(enemy.x, enemy.y);
|
||||||
enemy.destroy();
|
enemy.destroy();
|
||||||
|
sound.stomp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (player.body.velocity.y > 0 && player.body.bottom <= enemy.body.top + PHYSICS.stompTolerance) {
|
if (player.body.velocity.y > 0 && player.body.bottom <= enemy.body.top + PHYSICS.stompTolerance) {
|
||||||
this.createExplosion(enemy.x, enemy.y);
|
this.createExplosion(enemy.x, enemy.y);
|
||||||
enemy.destroy();
|
enemy.destroy();
|
||||||
player.jump();
|
player.jump();
|
||||||
|
sound.stomp();
|
||||||
this.scoreManager.addPoints(SCORE.stompBonus);
|
this.scoreManager.addPoints(SCORE.stompBonus);
|
||||||
this.createJumpParticles(player.x, player.y + player.displayHeight / 2 + 3);
|
this.createJumpParticles(player.x, player.y + player.displayHeight / 2 + 3);
|
||||||
return;
|
return;
|
||||||
@@ -177,11 +202,15 @@ export class GameScene extends Scene {
|
|||||||
if (this.isGameOver) return;
|
if (this.isGameOver) return;
|
||||||
this.isGameOver = true;
|
this.isGameOver = true;
|
||||||
this.player.die();
|
this.player.die();
|
||||||
|
sound.death();
|
||||||
this.cameras.main.shake(300, 0.012);
|
this.cameras.main.shake(300, 0.012);
|
||||||
const bx = ex ?? this.player.x;
|
const bx = ex ?? this.player.x;
|
||||||
const by = ey ?? this.player.y;
|
const by = ey ?? this.player.y;
|
||||||
this.createExplosion(bx, by);
|
this.createExplosion(bx, by);
|
||||||
const isNewBest = this.scoreManager.saveBest();
|
const isNewBest = this.scoreManager.saveBest();
|
||||||
|
if (isNewBest) {
|
||||||
|
this.time.delayedCall(400, () => sound.newBest());
|
||||||
|
}
|
||||||
const score = this.scoreManager.score;
|
const score = this.scoreManager.score;
|
||||||
const blockHeight = this.scoreManager.blockHeight;
|
const blockHeight = this.scoreManager.blockHeight;
|
||||||
|
|
||||||
@@ -191,6 +220,160 @@ export class GameScene extends Scene {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
togglePause() {
|
||||||
|
if (this.isGameOver) return;
|
||||||
|
this.isPaused = !this.isPaused;
|
||||||
|
if (this.isPaused) {
|
||||||
|
this.physics.pause();
|
||||||
|
this.pauseOverlay.setVisible(true);
|
||||||
|
sound.click();
|
||||||
|
} else {
|
||||||
|
this.physics.resume();
|
||||||
|
this.pauseOverlay.setVisible(false);
|
||||||
|
sound.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createPauseUI() {
|
||||||
|
const { width, height } = this.scale;
|
||||||
|
const container = this.add.container(0, 0).setScrollFactor(0).setDepth(500).setVisible(false);
|
||||||
|
|
||||||
|
const dim = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.65);
|
||||||
|
const panel = this.add.rectangle(width / 2, height / 2, 320, 280, 0x1a0533).setStrokeStyle(3, 0xa855f7);
|
||||||
|
|
||||||
|
const title = this.add.text(width / 2, height / 2 - 90, 'PAUSED', {
|
||||||
|
fontFamily: '"Press Start 2P", monospace',
|
||||||
|
fontSize: '22px',
|
||||||
|
color: '#d8b4fe',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
const resume = this.add.rectangle(width / 2, height / 2 - 20, 220, 44, 0x581c87)
|
||||||
|
.setStrokeStyle(2, 0xa855f7).setInteractive({ useHandCursor: true });
|
||||||
|
const resumeLabel = this.add.text(width / 2, height / 2 - 20, 'RESUME', {
|
||||||
|
fontFamily: '"Press Start 2P", monospace', fontSize: '13px', color: '#fff',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
resume.on('pointerdown', () => this.togglePause());
|
||||||
|
|
||||||
|
const quit = this.add.rectangle(width / 2, height / 2 + 40, 220, 44, 0x581c87)
|
||||||
|
.setStrokeStyle(2, 0xa855f7).setInteractive({ useHandCursor: true });
|
||||||
|
const quitLabel = this.add.text(width / 2, height / 2 + 40, 'MAIN MENU', {
|
||||||
|
fontFamily: '"Press Start 2P", monospace', fontSize: '13px', color: '#fff',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
quit.on('pointerdown', () => {
|
||||||
|
sound.click();
|
||||||
|
this.physics.resume();
|
||||||
|
this.scoreManager.destroy();
|
||||||
|
this.scene.start('MenuScene');
|
||||||
|
});
|
||||||
|
|
||||||
|
const hint = this.add.text(width / 2, height / 2 + 100, 'ESC to resume', {
|
||||||
|
fontFamily: '"Press Start 2P", monospace', fontSize: '9px', color: '#888',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
container.add([dim, panel, title, resume, resumeLabel, quit, quitLabel, hint]);
|
||||||
|
this.pauseOverlay = container;
|
||||||
|
|
||||||
|
const pauseBtn = this.add.text(GAME_WIDTH - 16, 50, '⏸', {
|
||||||
|
fontSize: '22px',
|
||||||
|
}).setOrigin(1, 0.5).setScrollFactor(0).setDepth(300).setInteractive({ useHandCursor: true });
|
||||||
|
pauseBtn.on('pointerdown', () => this.togglePause());
|
||||||
|
}
|
||||||
|
|
||||||
|
createMuteButton() {
|
||||||
|
const icon = this.add.text(GAME_WIDTH - 16, 80, sound.isMuted() ? '🔇' : '🔊', {
|
||||||
|
fontSize: '18px',
|
||||||
|
}).setOrigin(1, 0.5).setScrollFactor(0).setDepth(300).setInteractive({ useHandCursor: true });
|
||||||
|
icon.on('pointerdown', () => {
|
||||||
|
const next = !sound.isMuted();
|
||||||
|
sound.setMuted(next);
|
||||||
|
icon.setText(next ? '🔇' : '🔊');
|
||||||
|
if (!next) sound.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeShowTutorial() {
|
||||||
|
if (localStorage.getItem('naddie_tutorial_seen') === '1') return;
|
||||||
|
|
||||||
|
const { width, height } = this.scale;
|
||||||
|
this.isPaused = true;
|
||||||
|
this.physics.pause();
|
||||||
|
|
||||||
|
const container = this.add.container(0, 0).setScrollFactor(0).setDepth(600);
|
||||||
|
const dim = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.75);
|
||||||
|
const panel = this.add.rectangle(width / 2, height / 2, 380, 480, 0x1a0533).setStrokeStyle(3, 0xa855f7);
|
||||||
|
|
||||||
|
const title = this.add.text(width / 2, height * 0.22, 'HOW TO PLAY', {
|
||||||
|
fontFamily: '"Press Start 2P", monospace',
|
||||||
|
fontSize: '18px',
|
||||||
|
color: '#d8b4fe',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
'← → / A D — Move',
|
||||||
|
'TAP LEFT / RIGHT — Move',
|
||||||
|
'AUTO-JUMP on platforms',
|
||||||
|
'',
|
||||||
|
'PURPLE — Stable platform',
|
||||||
|
'GOLD — Genesis (x2 bonus)',
|
||||||
|
'GREY — Breaks on landing',
|
||||||
|
'MOVING — Slides side to side',
|
||||||
|
'',
|
||||||
|
'Avoid BUGS, grab POWER-UPS',
|
||||||
|
'ESC — Pause',
|
||||||
|
];
|
||||||
|
|
||||||
|
const texts = lines.map((l, i) => this.add.text(width / 2, height * 0.30 + i * 24, l, {
|
||||||
|
fontFamily: '"Press Start 2P", monospace',
|
||||||
|
fontSize: '10px',
|
||||||
|
color: l.includes('Genesis') ? '#ffd700' : l.includes('Breaks') ? '#999' : '#d8b4fe',
|
||||||
|
align: 'center',
|
||||||
|
}).setOrigin(0.5));
|
||||||
|
|
||||||
|
const startBtn = this.add.rectangle(width / 2, height * 0.85, 220, 48, 0x581c87)
|
||||||
|
.setStrokeStyle(2, 0xa855f7).setInteractive({ useHandCursor: true });
|
||||||
|
const startLabel = this.add.text(width / 2, height * 0.85, "LET'S JUMP!", {
|
||||||
|
fontFamily: '"Press Start 2P", monospace', fontSize: '14px', color: '#fff',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
container.add([dim, panel, title, ...texts, startBtn, startLabel]);
|
||||||
|
|
||||||
|
startBtn.on('pointerdown', () => {
|
||||||
|
sound.click();
|
||||||
|
localStorage.setItem('naddie_tutorial_seen', '1');
|
||||||
|
container.destroy();
|
||||||
|
this.isPaused = false;
|
||||||
|
this.physics.resume();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showTouchIndicators() {
|
||||||
|
if (!this.sys.game.device.input.touch) return;
|
||||||
|
const { width, height } = this.scale;
|
||||||
|
|
||||||
|
const left = this.add.rectangle(width / 4, height - 100, width / 2 - 20, 60, 0xa855f7, 0.15)
|
||||||
|
.setScrollFactor(0).setDepth(250);
|
||||||
|
const leftText = this.add.text(width / 4, height - 100, '◀ TAP', {
|
||||||
|
fontFamily: '"Press Start 2P", monospace', fontSize: '12px', color: '#fff',
|
||||||
|
}).setOrigin(0.5).setScrollFactor(0).setDepth(251).setAlpha(0.7);
|
||||||
|
|
||||||
|
const right = this.add.rectangle(3 * width / 4, height - 100, width / 2 - 20, 60, 0xa855f7, 0.15)
|
||||||
|
.setScrollFactor(0).setDepth(250);
|
||||||
|
const rightText = this.add.text(3 * width / 4, height - 100, 'TAP ▶', {
|
||||||
|
fontFamily: '"Press Start 2P", monospace', fontSize: '12px', color: '#fff',
|
||||||
|
}).setOrigin(0.5).setScrollFactor(0).setDepth(251).setAlpha(0.7);
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: [left, right, leftText, rightText],
|
||||||
|
alpha: 0,
|
||||||
|
delay: 3500,
|
||||||
|
duration: 1500,
|
||||||
|
onComplete: () => {
|
||||||
|
left.destroy(); right.destroy();
|
||||||
|
leftText.destroy(); rightText.destroy();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
flashScreen() {
|
flashScreen() {
|
||||||
const flash = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0xffffff, 0.3);
|
const flash = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0xffffff, 0.3);
|
||||||
flash.setScrollFactor(0);
|
flash.setScrollFactor(0);
|
||||||
@@ -228,7 +411,6 @@ export class GameScene extends Scene {
|
|||||||
Phaser.Math.Between(0, GAME_HEIGHT),
|
Phaser.Math.Between(0, GAME_HEIGHT),
|
||||||
'gwei'
|
'gwei'
|
||||||
).setAlpha(0.5).setScrollFactor(0.3).setDepth(-8);
|
).setAlpha(0.5).setScrollFactor(0.3).setDepth(-8);
|
||||||
const scene = this;
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: p,
|
targets: p,
|
||||||
y: p.y - Phaser.Math.Between(100, 400),
|
y: p.y - Phaser.Math.Between(100, 400),
|
||||||
@@ -236,7 +418,7 @@ export class GameScene extends Scene {
|
|||||||
duration: Phaser.Math.Between(3000, 7000),
|
duration: Phaser.Math.Between(3000, 7000),
|
||||||
repeat: -1,
|
repeat: -1,
|
||||||
delay: Phaser.Math.Between(0, 4000),
|
delay: Phaser.Math.Between(0, 4000),
|
||||||
onRepeat: function() {
|
onRepeat: () => {
|
||||||
p.y = Phaser.Math.Between(0, GAME_HEIGHT);
|
p.y = Phaser.Math.Between(0, GAME_HEIGHT);
|
||||||
p.x = Phaser.Math.Between(0, GAME_WIDTH);
|
p.x = Phaser.Math.Between(0, GAME_WIDTH);
|
||||||
p.setAlpha(0.5);
|
p.setAlpha(0.5);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Scene } from 'phaser';
|
import { Scene } from 'phaser';
|
||||||
|
import { sound } from '../managers/SoundManager.js';
|
||||||
|
|
||||||
export class MenuScene extends Scene {
|
export class MenuScene extends Scene {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -8,10 +9,8 @@ export class MenuScene extends Scene {
|
|||||||
create() {
|
create() {
|
||||||
const { width, height } = this.scale;
|
const { width, height } = this.scale;
|
||||||
|
|
||||||
// Background
|
|
||||||
this.add.tileSprite(width / 2, height / 2, width, height, 'gridBg');
|
this.add.tileSprite(width / 2, height / 2, width, height, 'gridBg');
|
||||||
|
|
||||||
// Title
|
|
||||||
this.add.text(width / 2, height * 0.18, 'NADDIE JUMP', {
|
this.add.text(width / 2, height * 0.18, 'NADDIE JUMP', {
|
||||||
fontFamily: '"Press Start 2P", monospace',
|
fontFamily: '"Press Start 2P", monospace',
|
||||||
fontSize: '38px',
|
fontSize: '38px',
|
||||||
@@ -26,51 +25,27 @@ export class MenuScene extends Scene {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
// Floating Naddie preview
|
const preview = this.add.image(width / 2, height * 0.48, 'player_idle').setScale(0.55);
|
||||||
const preview = this.add.image(width / 2, height * 0.48, 'player_idle')
|
this.tweens.add({ targets: preview, y: height * 0.48 - 15, duration: 1400, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' });
|
||||||
.setScale(0.55);
|
this.tweens.add({ targets: preview, angle: 5, duration: 2000, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' });
|
||||||
this.tweens.add({
|
|
||||||
targets: preview,
|
|
||||||
y: height * 0.48 - 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',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start button
|
this.createButton(width / 2, height * 0.68, 'START GAME', () => this.startGame());
|
||||||
this.createButton(width / 2, height * 0.68, 'START GAME', () => {
|
|
||||||
this.scene.start('GameScene');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.add.text(width / 2, height * 0.74, 'Press ENTER or SPACE to start', {
|
this.add.text(width / 2, height * 0.74, 'ENTER / SPACE / TAP', {
|
||||||
fontFamily: '"Press Start 2P", monospace',
|
fontFamily: '"Press Start 2P", monospace',
|
||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
color: '#888',
|
color: '#888',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
this.input.keyboard.on('keydown-ENTER', () => {
|
this.input.keyboard.on('keydown-ENTER', () => this.startGame());
|
||||||
this.scene.start('GameScene');
|
this.input.keyboard.on('keydown-SPACE', () => this.startGame());
|
||||||
});
|
|
||||||
this.input.keyboard.on('keydown-SPACE', () => {
|
|
||||||
this.scene.start('GameScene');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Leaderboard button
|
this.createButton(width / 2, height * 0.82, 'LEADERBOARD', () => this.showLeaderboard());
|
||||||
this.createButton(width / 2, height * 0.78, 'LEADERBOARD', () => {
|
|
||||||
this.showLeaderboard();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.add.text(width / 2, height * 0.92, 'Web3 integration coming soon', {
|
this.createMuteButton();
|
||||||
|
|
||||||
|
this.add.text(width / 2, height * 0.94, 'Web3 integration coming soon', {
|
||||||
fontFamily: '"Press Start 2P", monospace',
|
fontFamily: '"Press Start 2P", monospace',
|
||||||
fontSize: '9px',
|
fontSize: '9px',
|
||||||
color: '#444',
|
color: '#444',
|
||||||
@@ -78,6 +53,22 @@ export class MenuScene extends Scene {
|
|||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
this.createAmbientParticles();
|
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');
|
||||||
}
|
}
|
||||||
|
|
||||||
createButton(x, y, text, callback) {
|
createButton(x, y, text, callback) {
|
||||||
@@ -99,11 +90,31 @@ export class MenuScene extends Scene {
|
|||||||
bg.setFillStyle(0x581c87);
|
bg.setFillStyle(0x581c87);
|
||||||
this.tweens.add({ targets: [bg, label], scaleX: 1, scaleY: 1, duration: 100 });
|
this.tweens.add({ targets: [bg, label], scaleX: 1, scaleY: 1, duration: 100 });
|
||||||
});
|
});
|
||||||
bg.on('pointerdown', callback);
|
bg.on('pointerdown', () => {
|
||||||
|
sound.click();
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
|
||||||
return { bg, label };
|
return { bg, label };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createMuteButton() {
|
||||||
|
const { width } = this.scale;
|
||||||
|
const x = width - 30;
|
||||||
|
const y = 30;
|
||||||
|
const icon = this.add.text(x, y, 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() {
|
showLeaderboard() {
|
||||||
const { width, height } = this.scale;
|
const { width, height } = this.scale;
|
||||||
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.7).setDepth(100);
|
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.7).setDepth(100);
|
||||||
@@ -138,6 +149,7 @@ export class MenuScene extends Scene {
|
|||||||
});
|
});
|
||||||
|
|
||||||
close.on('pointerdown', () => {
|
close.on('pointerdown', () => {
|
||||||
|
sound.click();
|
||||||
overlay.destroy();
|
overlay.destroy();
|
||||||
panel.destroy();
|
panel.destroy();
|
||||||
title.destroy();
|
title.destroy();
|
||||||
|
|||||||
Reference in New Issue
Block a user