Compare commits

..

3 Commits

Author SHA1 Message Date
d509f1df4a 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
2026-05-23 17:00:56 +07:00
57f9e2f282 Sprint 2: gameplay — achievements, score popups, new enemy, polish
- AchievementsManager with 10 unlockables and toast popups
  (Genesis Block, Chain Reaction x3, Bug Hunter, First Flight,
   Liftoff, Power Trip, Survivor 500, Skyscraper 1000,
   Speedrun 100/60s, Gas Baron 50k)
- Score popups: +N text floats above player on every landing
  (gold and larger for genesis platforms)
- Powerup duration bar in HUD bottom, color-coded per power-up,
  uses scaleX for smooth depletion animation
- New enemy: Failed Tx, falls from above with sine drift, unlocks
  at difficulty > 800, can be stomped, tinted red
- Dynamic background: dark cosmic overlay alpha scales with height
  (max 0.5 at very high altitudes)
- Achievement hooks integrated into ScoreManager, GameScene
- Combo no longer resets if combo was already 0 (was triggering log spam)
2026-05-23 16:58:36 +07:00
fd93da0a71 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
2026-05-23 16:55:05 +07:00
9 changed files with 759 additions and 109 deletions

View File

@@ -8,29 +8,46 @@ export class Enemy extends Physics.Arcade.Sprite {
scene.physics.add.existing(this); scene.physics.add.existing(this);
this.enemyType = type; this.enemyType = type;
this.setScale(0.7);
this.body.allowGravity = false; this.body.allowGravity = false;
this.body.setSize(90, 80);
this.body.setOffset(35, 30);
if (type === 'bug') { if (type === 'bug') {
this.setScale(0.7);
this.body.setSize(this.width * 0.7, this.height * 0.7);
this.body.setOffset(this.width * 0.15, this.height * 0.15);
this.speed = Phaser.Math.Between(60, 140) * (Math.random() < 0.5 ? 1 : -1); this.speed = Phaser.Math.Between(60, 140) * (Math.random() < 0.5 ? 1 : -1);
this.startX = x; this.startX = x;
this.patrolRange = Phaser.Math.Between(80, 200); this.patrolRange = Phaser.Math.Between(80, 200);
} else if (type === 'failed_tx') {
this.setScale(0.55);
this.setTint(0xef4444);
this.body.setSize(this.width * 0.6, this.height * 0.6);
this.body.setOffset(this.width * 0.2, this.height * 0.2);
this.fallSpeed = Phaser.Math.Between(80, 140);
this.driftAmplitude = Phaser.Math.Between(20, 60);
this.driftFreq = Phaser.Math.FloatBetween(0.001, 0.003);
this.spawnTime = scene.time.now;
this.spawnX = x;
} }
} }
preUpdate(time, delta) { preUpdate(time, delta) {
super.preUpdate(time, delta); super.preUpdate(time, delta);
if (this.enemyType === 'bug') { if (this.enemyType === 'bug') {
this.x += this.speed * (delta / 1000); this.x += this.speed * (delta / 1000);
if (Math.abs(this.x - this.startX) > this.patrolRange) { if (Math.abs(this.x - this.startX) > this.patrolRange) {
this.speed *= -1; this.speed *= -1;
this.setFlipX(this.speed < 0); this.setFlipX(this.speed < 0);
} }
// Wrap
if (this.x < -60) this.x = GAME_WIDTH + 60; if (this.x < -60) this.x = GAME_WIDTH + 60;
if (this.x > GAME_WIDTH + 60) this.x = -60; if (this.x > GAME_WIDTH + 60) this.x = -60;
} else if (this.enemyType === 'failed_tx') {
const dt = delta / 1000;
this.y += this.fallSpeed * dt;
const t = time - this.spawnTime;
this.x = this.spawnX + Math.sin(t * this.driftFreq) * this.driftAmplitude;
this.x = Phaser.Math.Clamp(this.x, 30, GAME_WIDTH - 30);
this.setAngle(Math.sin(t * 0.005) * 15);
} }
} }
} }

View File

@@ -0,0 +1,149 @@
import { sound } from './SoundManager.js';
const STORAGE_KEY = 'naddie_achievements_v1';
const DEFS = [
{ id: 'genesis_block', title: 'Genesis Block', desc: 'Land on a gold platform' },
{ id: 'chain_reaction', title: 'Chain Reaction', desc: 'Reach combo x3.0' },
{ id: 'bug_hunter', title: 'Bug Hunter', desc: 'Stomp 5 enemies in one run' },
{ id: 'first_flight', title: 'First Flight', desc: 'Use the propeller' },
{ id: 'liftoff', title: 'Liftoff', desc: 'Use the rocket' },
{ id: 'power_trip', title: 'Power Trip', desc: 'Use all 3 power-ups in one run' },
{ id: 'survivor', title: 'Survivor', desc: 'Reach block height 500' },
{ id: 'skyscraper', title: 'Skyscraper', desc: 'Reach block height 1000' },
{ id: 'speedrun', title: 'Speedrun', desc: 'Reach block 100 in under 60s' },
{ id: 'gas_baron', title: 'Gas Baron', desc: 'Score 50,000 in one run' },
];
export class AchievementsManager {
constructor(scene) {
this.scene = scene;
this.unlocked = this._load();
this.popupQueue = [];
this.popupActive = false;
// Per-run counters
this.bugsStomped = 0;
this.powerupsUsed = new Set();
this.startTime = Date.now();
}
static getAll() {
return DEFS;
}
_load() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : {};
} catch (_) {
return {};
}
}
_save() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.unlocked));
} catch (_) {}
}
isUnlocked(id) {
return !!this.unlocked[id];
}
unlock(id) {
if (this.unlocked[id]) return;
const def = DEFS.find((d) => d.id === id);
if (!def) return;
this.unlocked[id] = Date.now();
this._save();
this.popupQueue.push(def);
this._processQueue();
sound.powerup();
}
_processQueue() {
if (this.popupActive || this.popupQueue.length === 0) return;
this.popupActive = true;
const def = this.popupQueue.shift();
this._showPopup(def, () => {
this.popupActive = false;
this._processQueue();
});
}
_showPopup(def, onDone) {
const { width } = this.scene.scale;
const y = 110;
const panel = this.scene.add.rectangle(width / 2, y, 320, 70, 0x1a0533, 0.92)
.setStrokeStyle(2, 0xffd700)
.setScrollFactor(0)
.setDepth(800);
const title = this.scene.add.text(width / 2, y - 14, `ACHIEVEMENT: ${def.title}`, {
fontFamily: '"Press Start 2P", monospace',
fontSize: '10px',
color: '#ffd700',
}).setOrigin(0.5).setScrollFactor(0).setDepth(801);
const desc = this.scene.add.text(width / 2, y + 10, def.desc, {
fontFamily: '"Press Start 2P", monospace',
fontSize: '8px',
color: '#d8b4fe',
}).setOrigin(0.5).setScrollFactor(0).setDepth(801);
panel.setAlpha(0);
title.setAlpha(0);
desc.setAlpha(0);
this.scene.tweens.add({
targets: [panel, title, desc],
alpha: 1,
y: `+=10`,
duration: 250,
ease: 'Quad.easeOut',
});
this.scene.time.delayedCall(2500, () => {
this.scene.tweens.add({
targets: [panel, title, desc],
alpha: 0,
duration: 350,
onComplete: () => {
panel.destroy(); title.destroy(); desc.destroy();
onDone();
},
});
});
}
// --- Game event hooks ---
onPlatformLand(type, combo) {
if (type === 'genesis') this.unlock('genesis_block');
if (combo >= 3) this.unlock('chain_reaction');
}
onEnemyStomp() {
this.bugsStomped += 1;
if (this.bugsStomped >= 5) this.unlock('bug_hunter');
}
onPowerup(name) {
this.powerupsUsed.add(name);
if (name === 'propeller') this.unlock('first_flight');
if (name === 'rocket') this.unlock('liftoff');
if (this.powerupsUsed.size >= 3) this.unlock('power_trip');
}
onBlockHeight(blocks) {
if (blocks >= 500) this.unlock('survivor');
if (blocks >= 1000) this.unlock('skyscraper');
if (blocks >= 100) {
const elapsed = (Date.now() - this.startTime) / 1000;
if (elapsed <= 60) this.unlock('speedrun');
}
}
onScore(score) {
if (score >= 50000) this.unlock('gas_baron');
}
}

View File

@@ -92,8 +92,12 @@ export class PlatformManager {
Math.floor(difficultyLevel / 1000) * DIFFICULTY.enemyIncreasePer1000, Math.floor(difficultyLevel / 1000) * DIFFICULTY.enemyIncreasePer1000,
DIFFICULTY.maxEnemyRate DIFFICULTY.maxEnemyRate
); );
const bugRate = Math.min(ENEMY_RATES.bug + enemyBonus, DIFFICULTY.maxEnemyRate); const totalRate = Math.min(ENEMY_RATES.bug + enemyBonus, DIFFICULTY.maxEnemyRate);
if (Math.random() >= bugRate) return; if (Math.random() >= totalRate) return;
// Failed-Tx type unlocks at higher difficulty
const failedTxAllowed = difficultyLevel > 800;
const type = failedTxAllowed && Math.random() < 0.30 ? 'failed_tx' : 'bug';
const offset = Phaser.Math.Between(-60, 60); const offset = Phaser.Math.Between(-60, 60);
const ex = Phaser.Math.Clamp(platformX + offset, 50, GAME_WIDTH - 50); const ex = Phaser.Math.Clamp(platformX + offset, 50, GAME_WIDTH - 50);
@@ -108,7 +112,7 @@ export class PlatformManager {
}); });
if (tooClose) return; if (tooClose) return;
this.enemies.add(new Enemy(this.scene, ex, ey, 'bug')); this.enemies.add(new Enemy(this.scene, ex, ey, type));
} }
getPlatforms() { getPlatforms() {

View File

@@ -39,6 +39,18 @@ export class ScoreManager {
color: '#aaa', color: '#aaa',
}).setOrigin(1, 0).setScrollFactor(0).setDepth(200).setShadow(1, 1, '#000000', 2, false, true); }).setOrigin(1, 0).setScrollFactor(0).setDepth(200).setShadow(1, 1, '#000000', 2, false, true);
// Powerup duration bar (hidden until active)
this.powerupBarBg = scene.add.rectangle(scene.scale.width / 2, scene.scale.height - 24, 200, 12, 0x000000, 0.6)
.setStrokeStyle(2, 0xa855f7)
.setScrollFactor(0).setDepth(200).setVisible(false);
this.powerupBarFill = scene.add.rectangle(scene.scale.width / 2 - 99, scene.scale.height - 24, 196, 8, 0xa855f7)
.setOrigin(0, 0.5).setScrollFactor(0).setDepth(201).setVisible(false);
this.powerupBarLabel = scene.add.text(scene.scale.width / 2, scene.scale.height - 40, '', {
fontFamily: '"Press Start 2P", monospace',
fontSize: '9px',
color: '#d8b4fe',
}).setOrigin(0.5).setScrollFactor(0).setDepth(201).setVisible(false);
this.updateBestDisplay(); this.updateBestDisplay();
} }
@@ -47,11 +59,51 @@ export class ScoreManager {
if (blocks > this.blockHeight) { if (blocks > this.blockHeight) {
this.blockHeight = blocks; this.blockHeight = blocks;
this.hudBlocks.setText(`Block: ${this.blockHeight}`); this.hudBlocks.setText(`Block: ${this.blockHeight}`);
if (this.scene.achievements) this.scene.achievements.onBlockHeight(this.blockHeight);
if (this.blockHeight % 100 === 0 && this.blockHeight > 0) { if (this.blockHeight % 100 === 0 && this.blockHeight > 0) {
this.showMilestone(this.blockHeight); this.showMilestone(this.blockHeight);
} }
} }
this._updatePowerupBar();
}
_updatePowerupBar() {
const player = this.scene.player;
if (!player) return;
let active = null;
let remaining = 0;
let total = 0;
let color = 0xa855f7;
let label = '';
if (player.state === 'propeller') {
active = 'propeller';
remaining = Math.max(0, player.propellerTimer);
total = 3500;
color = 0x44aaff;
label = 'PROPELLER';
} else if (player.state === 'rocket') {
active = 'rocket';
remaining = Math.max(0, player.rocketTimer);
total = 3000;
color = 0xff4444;
label = 'ROCKET';
}
if (active) {
const pct = remaining / total;
this.powerupBarBg.setVisible(true);
this.powerupBarFill.setVisible(true).setFillStyle(color);
this.powerupBarFill.scaleX = pct;
this.powerupBarLabel.setVisible(true).setText(label);
} else {
this.powerupBarBg.setVisible(false);
this.powerupBarFill.setVisible(false);
this.powerupBarLabel.setVisible(false);
}
} }
onLand(platformY, platformType) { onLand(platformY, platformType) {
@@ -72,7 +124,13 @@ export class ScoreManager {
if (this.genesisJumps <= 0) this.genesisActive = false; if (this.genesisJumps <= 0) this.genesisActive = false;
} }
this.addPoints(Math.floor(basePoints * multiplier)); const gained = Math.floor(basePoints * multiplier);
this.addPoints(gained);
this._spawnScorePopup(gained, platformType === 'genesis');
if (this.scene.achievements) {
this.scene.achievements.onPlatformLand(platformType, this.comboMultiplier);
}
if (this.combo > 1) { if (this.combo > 1) {
this.hudCombo.setText(`Combo x${this.comboMultiplier.toFixed(1)}`); this.hudCombo.setText(`Combo x${this.comboMultiplier.toFixed(1)}`);
@@ -80,15 +138,36 @@ export class ScoreManager {
} }
} }
_spawnScorePopup(amount, big) {
const player = this.scene.player;
if (!player) return;
const txt = this.scene.add.text(player.x, player.y - 30, `+${amount}`, {
fontFamily: '"Press Start 2P", monospace',
fontSize: big ? '14px' : '10px',
color: big ? '#ffd700' : '#22c55e',
}).setOrigin(0.5).setDepth(150);
this.scene.tweens.add({
targets: txt,
y: txt.y - 35,
alpha: 0,
duration: 700,
ease: 'Quad.easeOut',
onComplete: () => txt.destroy(),
});
}
addPoints(amount) { addPoints(amount) {
this.score += amount; this.score += amount;
this.hudScore.setText(`Gas: ${this.score}`); this.hudScore.setText(`Gas: ${this.score}`);
if (this.scene.achievements) this.scene.achievements.onScore(this.score);
} }
onFall() { onFall() {
this.combo = 0; if (this.combo > 0) {
this.comboMultiplier = 1; this.combo = 0;
this.hudCombo.setText(''); this.comboMultiplier = 1;
this.hudCombo.setText('');
}
} }
showMilestone(blocks) { showMilestone(blocks) {
@@ -133,5 +212,8 @@ export class ScoreManager {
this.hudBlocks.destroy(); this.hudBlocks.destroy();
this.hudCombo.destroy(); this.hudCombo.destroy();
this.hudBest.destroy(); this.hudBest.destroy();
this.powerupBarBg.destroy();
this.powerupBarFill.destroy();
this.powerupBarLabel.destroy();
} }
} }

View 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();

View File

@@ -1,4 +1,6 @@
import { Scene } from 'phaser'; import { Scene } from 'phaser';
import { sound } from '../managers/SoundManager.js';
import { createButton } from '../utils/ui.js';
export class GameOverScene extends Scene { export class GameOverScene extends Scene {
constructor() { constructor() {
@@ -16,19 +18,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 +29,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}`, {
@@ -56,13 +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'));
this.scene.start('GameScene'); createButton(this, width / 2, height * 0.82, 'MAIN MENU', () => this.scene.start('MenuScene'));
});
this.createButton(width / 2, height * 0.82, 'MAIN MENU', () => {
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',
@@ -70,20 +58,14 @@ export class GameOverScene extends Scene {
color: '#666', color: '#666',
align: 'center', align: 'center',
}).setOrigin(0.5); }).setOrigin(0.5);
}
createButton(x, y, text, callback) { this.input.keyboard.once('keydown-ENTER', () => {
const bg = this.add.rectangle(x, y, 260, 48, 0x581c87) sound.click();
.setStrokeStyle(2, 0xa855f7) this.scene.start('GameScene');
.setInteractive({ useHandCursor: true }); });
const label = this.add.text(x, y, text, { this.input.keyboard.once('keydown-SPACE', () => {
fontFamily: '"Press Start 2P", monospace', sound.click();
fontSize: '13px', this.scene.start('GameScene');
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

@@ -3,6 +3,8 @@ 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 { AchievementsManager } from '../managers/AchievementsManager.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,14 +13,22 @@ 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);
// Color overlay for height-based tint shift
this.bgTint = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000033, 0)
.setScrollFactor(0).setDepth(-9);
this.createGweiParticles(); this.createGweiParticles();
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();
@@ -37,6 +47,7 @@ export class GameScene extends Scene {
this.trailGraphics = this.add.graphics().setDepth(-3); this.trailGraphics = this.add.graphics().setDepth(-3);
this.platformManager = new PlatformManager(this); this.platformManager = new PlatformManager(this);
this.achievements = new AchievementsManager(this);
this.scoreManager = new ScoreManager(this); this.scoreManager = new ScoreManager(this);
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
@@ -52,12 +63,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);
@@ -68,6 +90,10 @@ export class GameScene extends Scene {
const height = Math.max(0, GAME_HEIGHT - this.player.y); const height = Math.max(0, GAME_HEIGHT - this.player.y);
this.difficultyLevel = height; this.difficultyLevel = height;
// Background darkens as player climbs higher (cosmic feel)
const tintAlpha = Math.min(0.5, height / 8000);
this.bgTint.fillAlpha = tintAlpha;
this.platformManager.update(this.difficultyLevel, killLine); this.platformManager.update(this.difficultyLevel, killLine);
this.scoreManager.update(this.player.y); this.scoreManager.update(this.player.y);
@@ -130,9 +156,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 +170,18 @@ 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 name = powerup.constructor.name.toLowerCase();
const isSpring = 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();
if (this.achievements) {
const map = { spring: 'spring', propellerhat: 'propeller', rocket: 'rocket' };
this.achievements.onPowerup(map[name] || name);
}
} }
} }
} }
@@ -154,19 +191,23 @@ 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);
if (this.achievements) this.achievements.onEnemyStomp();
this.createJumpParticles(player.x, player.y + player.displayHeight / 2 + 3); this.createJumpParticles(player.x, player.y + player.displayHeight / 2 + 3);
return; return;
} }
@@ -177,11 +218,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 +236,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 +427,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 +434,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);

View File

@@ -1,4 +1,7 @@
import { Scene } from 'phaser'; 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 { export class MenuScene extends Scene {
constructor() { constructor() {
@@ -8,69 +11,50 @@ 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.16, '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',
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);
// Floating Naddie preview const preview = this.add.image(width / 2, height * 0.44, '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.44 - 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, createButton(this, width / 2, height * 0.62, 'START GAME', () => this.startGame(), {
y: height * 0.48 - 15, width: 280, height: 56, fontSize: '15px',
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.add.text(width / 2, height * 0.68, 'ENTER / SPACE / TAP', {
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', {
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());
createButton(this, width / 2 - 75, height * 0.78, 'BOARD', () => this.showLeaderboard(), {
width: 140, height: 44, fontSize: '11px',
}); });
this.input.keyboard.on('keydown-SPACE', () => { createButton(this, width / 2 + 75, height * 0.78, 'BADGES', () => this.showAchievements(), {
this.scene.start('GameScene'); width: 140, height: 44, fontSize: '11px',
}); });
// Leaderboard button this.createMuteButton();
this.createButton(width / 2, height * 0.78, 'LEADERBOARD', () => {
this.showLeaderboard();
});
this.add.text(width / 2, height * 0.92, 'Web3 integration coming soon', { 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,30 +62,31 @@ 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(); });
} }
createButton(x, y, text, callback) { startGame() {
const bg = this.add.rectangle(x, y, 280, 56, 0x581c87) sound.init();
.setStrokeStyle(3, 0xa855f7) sound.resume();
.setInteractive({ useHandCursor: true }); sound.click();
this.scene.start('GameScene');
}
const label = this.add.text(x, y, text, { createMuteButton() {
fontFamily: '"Press Start 2P", monospace', const { width } = this.scale;
fontSize: '15px', const icon = this.add.text(width - 30, 30, sound.isMuted() ? '🔇' : '🔊', {
color: '#ffffff', fontSize: '24px',
}).setOrigin(0.5); }).setOrigin(0.5).setInteractive({ useHandCursor: true });
bg.on('pointerover', () => { icon.on('pointerdown', () => {
bg.setFillStyle(0x7e22ce); sound.init();
this.tweens.add({ targets: [bg, label], scaleX: 1.05, scaleY: 1.05, duration: 100 }); const next = !sound.isMuted();
sound.setMuted(next);
icon.setText(next ? '🔇' : '🔊');
if (!next) sound.click();
}); });
bg.on('pointerout', () => {
bg.setFillStyle(0x581c87);
this.tweens.add({ targets: [bg, label], scaleX: 1, scaleY: 1, duration: 100 });
});
bg.on('pointerdown', callback);
return { bg, label };
} }
showLeaderboard() { showLeaderboard() {
@@ -138,14 +123,65 @@ export class MenuScene extends Scene {
}); });
close.on('pointerdown', () => { close.on('pointerdown', () => {
overlay.destroy(); sound.click();
panel.destroy(); overlay.destroy(); panel.destroy(); title.destroy(); close.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);
}