Initial commit: Naddie Jump — Monad Edition

This commit is contained in:
2026-05-23 15:33:42 +07:00
commit 7439f8f267
57 changed files with 2765 additions and 0 deletions

43
src/config/game.config.js Normal file
View File

@@ -0,0 +1,43 @@
export const GAME_WIDTH = 480;
export const GAME_HEIGHT = 854;
export const GRAVITY = 1200;
export const JUMP_VELOCITY = -650;
export const SUPER_JUMP_VELOCITY = -950;
export const ROCKET_VELOCITY = -1400;
export const PROPELLER_VELOCITY = -950;
export const PLAYER_SPEED = 300;
export const PLAYER_MAX_SPEED = 400;
export const PLATFORM_GAP_MIN = 60;
export const PLATFORM_GAP_MAX = 120;
export const PLATFORM_WIDTH = 80;
export const PLATFORM_HEIGHT = 24;
export const SPAWN_RATES = {
stable: 0.60,
moving: 0.20,
breaking: 0.15,
genesis: 0.05,
};
export const POWERUP_RATES = {
none: 0.91,
spring: 0.04,
propeller: 0.025,
rocket: 0.015,
};
export const ENEMY_RATES = {
none: 0.90,
bug: 0.10,
};
export const DIFFICULTY = {
initialGap: 100,
gapIncreasePer1000: 12,
maxGap: 160,
enemyIncreasePer1000: 0.015,
maxEnemyRate: 0.25,
};

36
src/entities/Enemy.js Normal file
View File

@@ -0,0 +1,36 @@
import { Physics } from 'phaser';
import { GAME_WIDTH } from '../config/game.config.js';
export class Enemy extends Physics.Arcade.Sprite {
constructor(scene, x, y, type = 'bug') {
super(scene, x, y, 'enemy_bug');
scene.add.existing(this);
scene.physics.add.existing(this);
this.enemyType = type;
this.setScale(0.7);
this.body.allowGravity = false;
this.body.setSize(90, 80);
this.body.setOffset(35, 30);
if (type === 'bug') {
this.speed = Phaser.Math.Between(60, 140) * (Math.random() < 0.5 ? 1 : -1);
this.startX = x;
this.patrolRange = Phaser.Math.Between(80, 200);
}
}
preUpdate(time, delta) {
super.preUpdate(time, delta);
if (this.enemyType === 'bug') {
this.x += this.speed * (delta / 1000);
if (Math.abs(this.x - this.startX) > this.patrolRange) {
this.speed *= -1;
this.setFlipX(this.speed < 0);
}
// Wrap
if (this.x < -60) this.x = GAME_WIDTH + 60;
if (this.x > GAME_WIDTH + 60) this.x = -60;
}
}
}

80
src/entities/Platform.js Normal file
View File

@@ -0,0 +1,80 @@
import { Physics } from 'phaser';
export class Platform extends Physics.Arcade.Sprite {
constructor(scene, x, y, type = 'stable') {
super(scene, x, y, 'platform');
scene.add.existing(this);
scene.physics.add.existing(this, true); // static body by default
this.platformType = type;
this.breakingState = 0;
this.moveSpeed = 0;
this.moveRange = 0;
this.startX = x;
if (type === 'moving') {
// Convert to dynamic for movement
this.body.destroy();
this.body = new Phaser.Physics.Arcade.Body(scene.physics.world, this);
this.body.allowGravity = false;
this.body.immovable = true;
this.moveSpeed = Phaser.Math.Between(50, 120) * (Math.random() < 0.5 ? 1 : -1);
this.moveRange = Phaser.Math.Between(60, 160);
} else if (type === 'breaking') {
this.setTint(0x999999); // grey tint for broken look
} else if (type === 'genesis') {
this.setTint(0xffd700); // gold
this.genesisGlow = scene.add.ellipse(x, y + 10, 100, 30, 0xffd700, 0.25)
.setDepth(this.depth - 1);
scene.tweens.add({
targets: this.genesisGlow,
scaleX: 1.4,
scaleY: 1.4,
alpha: 0.1,
duration: 800,
yoyo: true,
repeat: -1,
});
}
}
preUpdate(time, delta) {
super.preUpdate(time, delta);
if (this.platformType === 'moving' && this.body) {
this.setVelocityX(this.moveSpeed);
if (Math.abs(this.x - this.startX) > this.moveRange) {
this.moveSpeed *= -1;
}
if (this.genesisGlow) this.genesisGlow.setPosition(this.x, this.y + 10);
}
}
onPlayerLand(player) {
if (this.platformType === 'breaking') {
if (this.breakingState > 0) return false;
this.breakingState = 2;
this.disableBody(true, false);
this.scene.tweens.add({
targets: this,
alpha: 0,
scaleX: 0.3,
scaleY: 0.1,
duration: 250,
onComplete: () => this.destroy(),
});
return false;
}
return true;
}
isBroken() {
return this.breakingState >= 2;
}
destroy(fromScene) {
if (this.glowTween) this.glowTween.stop();
if (this.genesisGlow) this.genesisGlow.destroy();
super.destroy(fromScene);
}
}

117
src/entities/Player.js Normal file
View File

@@ -0,0 +1,117 @@
import { Physics } from 'phaser';
import {
GAME_WIDTH,
JUMP_VELOCITY,
SUPER_JUMP_VELOCITY,
ROCKET_VELOCITY,
PROPELLER_VELOCITY,
PLAYER_SPEED,
} from '../config/game.config.js';
export class Player extends Physics.Arcade.Sprite {
constructor(scene, x, y) {
super(scene, x, y, 'player_idle');
scene.add.existing(this);
scene.physics.add.existing(this);
this.setCollideWorldBounds(false);
this.setScale(0.45);
this.body.setSize(70, 90);
this.state = 'normal'; // normal, propeller, rocket, dead
this.propellerTimer = 0;
this.rocketTimer = 0;
}
update(cursors, wasd, touchLeft, touchRight, time, delta) {
if (this.state === 'dead') return;
let velocityX = 0;
if (cursors.left.isDown || wasd.left.isDown || touchLeft) velocityX = -PLAYER_SPEED;
if (cursors.right.isDown || wasd.right.isDown || touchRight) velocityX = PLAYER_SPEED;
this.setVelocityX(velocityX);
// Screen wrap
if (this.x < -this.width / 2) this.x = GAME_WIDTH + this.width / 2;
if (this.x > GAME_WIDTH + this.width / 2) this.x = -this.width / 2;
// Tilt sprite based on movement
if (velocityX < 0) {
this.setFlipX(true);
this.setAngle(-5);
} else if (velocityX > 0) {
this.setFlipX(false);
this.setAngle(5);
} else {
this.setAngle(0);
}
// Power-up timers
if (this.state === 'propeller') {
this.propellerTimer -= delta;
this.setVelocityY(PROPELLER_VELOCITY);
if (this.propellerTimer <= 0) this.endPowerUp();
} else if (this.state === 'rocket') {
this.rocketTimer -= delta;
this.setVelocityY(ROCKET_VELOCITY);
if (this.rocketTimer <= 0) this.endPowerUp();
}
}
jump(force = JUMP_VELOCITY) {
if (this.state === 'dead' || this.state === 'rocket' || this.state === 'propeller') return;
this.setVelocityY(force);
// Squash animation
this.scene.tweens.add({
targets: this,
scaleX: 0.55,
scaleY: 0.4,
duration: 80,
yoyo: true,
ease: 'Quad.easeOut',
});
}
startPropeller() {
if (this.state === 'dead') return;
this.state = 'propeller';
this.propellerTimer = 3500;
const oldHeight = this.displayHeight;
this.setTexture('player_propeller');
this.setScale(0.45);
this.body.setSize(70, 105);
this.y -= (this.displayHeight - oldHeight) / 2;
}
startRocket() {
if (this.state === 'dead') return;
this.state = 'rocket';
this.rocketTimer = 4000;
const oldHeight = this.displayHeight;
this.setTexture('player_rocket');
this.setScale(0.52);
this.body.setSize(55, 180);
this.y -= (this.displayHeight - oldHeight) / 2;
}
endPowerUp() {
const oldHeight = this.displayHeight;
this.state = 'normal';
this.setTexture('player_idle');
this.setScale(0.45);
this.body.setSize(70, 90);
this.setAngle(0);
this.y -= (this.displayHeight - oldHeight) / 2;
}
die() {
if (this.state === 'dead') return;
this.state = 'dead';
this.setTexture('player_dead');
this.setScale(0.4);
this.setAngle(0);
this.body.setSize(70, 90);
this.setVelocity(0, -250);
this.body.allowGravity = true;
}
}

View File

@@ -0,0 +1,21 @@
import { Physics } from 'phaser';
export class PropellerHat extends Physics.Arcade.Sprite {
constructor(scene, x, y) {
super(scene, x, y, 'propeller_hat');
scene.add.existing(this);
scene.physics.add.existing(this, true);
this.setScale(0.45);
this.body.setSize(50, 40);
this.body.setOffset(48, 43);
// No spin — static propeller hat
}
onPlayerTouch(player) {
this.destroy();
player.startPropeller();
return true;
}
}

29
src/entities/Rocket.js Normal file
View File

@@ -0,0 +1,29 @@
import { Physics } from 'phaser';
export class Rocket extends Physics.Arcade.Sprite {
constructor(scene, x, y) {
super(scene, x, y, 'rocket');
scene.add.existing(this);
scene.physics.add.existing(this, true);
this.setScale(0.45);
this.body.setSize(80, 100, false);
this.body.setOffset(59, 91);
// Float animation
scene.tweens.add({
targets: this,
y: y - 8,
duration: 600,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
});
}
onPlayerTouch(player) {
this.destroy();
player.startRocket();
return true;
}
}

23
src/entities/Spring.js Normal file
View File

@@ -0,0 +1,23 @@
import { Physics } from 'phaser';
import { SUPER_JUMP_VELOCITY } from '../config/game.config.js';
export class Spring extends Physics.Arcade.Sprite {
constructor(scene, x, y) {
super(scene, x, y, 'spring');
scene.add.existing(this);
scene.physics.add.existing(this, true);
this.setScale(1);
this.body.setSize(20, 32);
this.active = true;
}
onPlayerTouch(player) {
if (!this.active) return false;
this.active = false;
this.setVisible(false);
this.body.enable = false;
player.jump(SUPER_JUMP_VELOCITY);
return true;
}
}

31
src/main.js Normal file
View File

@@ -0,0 +1,31 @@
import { Game, AUTO } from 'phaser';
import { BootScene } from './scenes/BootScene.js';
import { MenuScene } from './scenes/MenuScene.js';
import { GameScene } from './scenes/GameScene.js';
import { GameOverScene } from './scenes/GameOverScene.js';
import { GAME_WIDTH, GAME_HEIGHT } from './config/game.config.js';
const config = {
type: AUTO,
parent: 'game-container',
width: GAME_WIDTH,
height: GAME_HEIGHT,
backgroundColor: '#0d001a',
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
physics: {
default: 'arcade',
arcade: {
gravity: { y: 1200 },
debug: false,
tileBias: 64,
},
},
scene: [BootScene, MenuScene, GameScene, GameOverScene],
pixelArt: false,
antialias: true,
};
window.game = new Game(config);

View File

@@ -0,0 +1,52 @@
/**
* BlockchainManager — abstraction layer for Web3 integration.
* Currently a stub with localStorage fallback.
* Future: replace with viem/ethers calls to Monad.
*/
export class BlockchainManager {
constructor() {
this.connected = false;
this.address = null;
this.bestScore = this.loadLocalBest();
}
async connect() {
// TODO: integrate wallet (MetaMask, Rainbow, etc.) via viem
// connect() stub
this.connected = true;
this.address = '0xStub...' + Math.random().toString(36).slice(2, 8);
return this.address;
}
disconnect() {
this.connected = false;
this.address = null;
}
async submitScore(score) {
// submitScore() stub
if (score > this.bestScore) {
this.bestScore = score;
this.saveLocalBest(score);
}
return { txHash: '0x' + Math.random().toString(16).slice(2), confirmed: true };
}
async getLeaderboard(limit = 10) {
// getLeaderboard() stub
return [
{ rank: 1, address: '0xMonad...Dev', score: 99999 },
{ rank: 2, address: '0xAlice...xyz', score: 87500 },
{ rank: 3, address: '0xBob...abc', score: 74200 },
];
}
loadLocalBest() {
const raw = localStorage.getItem('naddie_best_score');
return raw ? parseInt(raw, 10) : 0;
}
saveLocalBest(score) {
localStorage.setItem('naddie_best_score', String(score));
}
}

View File

@@ -0,0 +1,131 @@
import { Platform } from '../entities/Platform.js';
import { Spring } from '../entities/Spring.js';
import { PropellerHat } from '../entities/PropellerHat.js';
import { Rocket } from '../entities/Rocket.js';
import { Enemy } from '../entities/Enemy.js';
import {
GAME_WIDTH, PLATFORM_GAP_MIN, PLATFORM_GAP_MAX,
SPAWN_RATES, POWERUP_RATES, ENEMY_RATES, DIFFICULTY,
} from '../config/game.config.js';
export class PlatformManager {
constructor(scene) {
this.scene = scene;
this.platforms = scene.add.group({ classType: Platform });
this.enemies = scene.add.group({ classType: Enemy });
this.powerups = scene.add.group();
this.lastY = scene.scale.height - 80;
this.highestY = this.lastY;
}
update(difficultyLevel, killLine) {
const camY = this.scene.cameras.main.scrollY;
let maxPerFrame = 5;
while (this.highestY > camY - 250 && maxPerFrame-- > 0) {
this.spawnPlatform(difficultyLevel);
}
// Cleanup below kill line — destroy when object's bottom crosses it
this.platforms.children.iterate((p) => {
if (p && p.y + p.displayHeight / 2 > killLine) {
p.destroy();
}
});
this.enemies.children.iterate((e) => {
if (e && e.y + e.displayHeight / 2 > killLine) {
e.destroy();
}
});
this.powerups.children.iterate((pu) => {
if (pu && pu.y + pu.displayHeight / 2 > killLine) {
pu.destroy();
}
});
}
spawnPlatform(difficultyLevel) {
const gapIncrease = Math.min(
Math.floor(difficultyLevel / 1000) * DIFFICULTY.gapIncreasePer1000,
DIFFICULTY.maxGap - DIFFICULTY.initialGap
);
const gap = Phaser.Math.Between(
PLATFORM_GAP_MIN + gapIncrease,
Math.min(PLATFORM_GAP_MAX + gapIncrease, DIFFICULTY.maxGap)
);
this.highestY -= gap;
const x = Phaser.Math.Between(60, GAME_WIDTH - 60);
const rand = Math.random();
let type = 'stable';
if (rand < SPAWN_RATES.stable) type = 'stable';
else if (rand < SPAWN_RATES.stable + SPAWN_RATES.moving) type = 'moving';
else if (rand < SPAWN_RATES.stable + SPAWN_RATES.moving + SPAWN_RATES.breaking) type = 'breaking';
else type = 'genesis';
const platform = new Platform(this.scene, x, this.highestY, type);
this.platforms.add(platform);
this.maybeSpawnPowerUp(x, this.highestY - 25);
this.maybeSpawnEnemy(x, this.highestY, difficultyLevel);
}
maybeSpawnPowerUp(x, y) {
const rand = Math.random();
let type = 'none';
if (rand < POWERUP_RATES.spring) type = 'spring';
else if (rand < POWERUP_RATES.spring + POWERUP_RATES.propeller) type = 'propeller';
else if (rand < POWERUP_RATES.spring + POWERUP_RATES.propeller + POWERUP_RATES.rocket) type = 'rocket';
if (type === 'none') return;
const offsetX = Phaser.Math.Between(-25, 25);
if (type === 'spring') {
this.powerups.add(new Spring(this.scene, x + offsetX, y));
} else if (type === 'propeller') {
this.powerups.add(new PropellerHat(this.scene, x + offsetX, y - 35));
} else if (type === 'rocket') {
this.powerups.add(new Rocket(this.scene, x + offsetX, y - 45));
}
}
maybeSpawnEnemy(x, y, difficultyLevel) {
const enemyBonus = Math.min(
Math.floor(difficultyLevel / 1000) * DIFFICULTY.enemyIncreasePer1000,
DIFFICULTY.maxEnemyRate
);
const bugRate = Math.min(ENEMY_RATES.bug + enemyBonus, 0.5);
const rand = Math.random();
let type = 'none';
if (rand < bugRate) type = 'bug';
if (type === 'none') return;
const ex = Phaser.Math.Between(50, GAME_WIDTH - 50);
const ey = y - Phaser.Math.Between(50, 140);
// Ensure minimum distance from existing enemies
const minDist = 120;
let tooClose = false;
this.enemies.children.iterate((e) => {
if (e && Phaser.Math.Distance.Between(ex, ey, e.x, e.y) < minDist) {
tooClose = true;
}
});
if (tooClose) return;
this.enemies.add(new Enemy(this.scene, ex, ey, type));
}
getPlatforms() {
return this.platforms;
}
getEnemies() {
return this.enemies;
}
getPowerups() {
return this.powerups;
}
}

View File

@@ -0,0 +1,137 @@
export class ScoreManager {
constructor(scene) {
this.scene = scene;
this.score = 0;
this.blockHeight = 0;
this.combo = 0;
this.comboMultiplier = 1;
this.lastPlatformY = null;
this.genesisActive = false;
this.genesisJumps = 0;
this.hudBg = scene.add.rectangle(0, 0, 200, 90, 0x000000, 0.5)
.setOrigin(0)
.setDepth(200);
this.hudScore = scene.add.text(16, 16, 'Gas: 0', {
fontFamily: '"Press Start 2P", monospace',
fontSize: '14px',
color: '#d8b4fe',
}).setOrigin(0).setDepth(200).setShadow(1, 1, '#000000', 2, false, true);
this.hudBlocks = scene.add.text(16, 42, 'Block: 0', {
fontFamily: '"Press Start 2P", monospace',
fontSize: '12px',
color: '#a855f7',
}).setOrigin(0).setDepth(200).setShadow(1, 1, '#000000', 2, false, true);
this.hudCombo = scene.add.text(16, 66, '', {
fontFamily: '"Press Start 2P", monospace',
fontSize: '10px',
color: '#22c55e',
}).setOrigin(0).setDepth(200).setShadow(1, 1, '#000000', 2, false, true);
this.hudBest = scene.add.text(scene.scale.width - 80, 16, 'Best: 0', {
fontFamily: '"Press Start 2P", monospace',
fontSize: '9px',
color: '#666',
}).setOrigin(1, 0).setDepth(200).setShadow(1, 1, '#000000', 2, false, true);
this.updateBestDisplay();
}
update(playerY) {
const cam = this.scene.cameras.main;
const width = this.scene.scale.width;
this.hudBg.setPosition(cam.scrollX + 10, cam.scrollY + 10);
this.hudScore.setPosition(cam.scrollX + 16, cam.scrollY + 16);
this.hudBlocks.setPosition(cam.scrollX + 16, cam.scrollY + 42);
this.hudCombo.setPosition(cam.scrollX + 16, cam.scrollY + 66);
this.hudBest.setPosition(cam.scrollX + width - 80, cam.scrollY + 16);
const blocks = Math.max(0, Math.floor((this.scene.scale.height - playerY) / 100));
if (blocks > this.blockHeight) {
this.blockHeight = blocks;
this.hudBlocks.setText(`Block: ${this.blockHeight}`);
// Milestone effect every 100 blocks
if (this.blockHeight % 100 === 0 && this.blockHeight > 0) {
this.showMilestone(this.blockHeight);
}
}
}
onLand(platformY, platformType) {
this.combo += 1;
this.comboMultiplier = Math.min(1 + (this.combo - 1) * 0.1, 3);
let basePoints = 10;
if (platformType === 'genesis') {
this.genesisActive = true;
this.genesisJumps = 5;
basePoints = 50;
}
let multiplier = this.comboMultiplier;
if (this.genesisActive) {
multiplier *= 2;
this.genesisJumps--;
if (this.genesisJumps <= 0) this.genesisActive = false;
}
this.score += Math.floor(basePoints * multiplier);
this.hudScore.setText(`Gas: ${this.score}`);
if (this.combo > 1) {
this.hudCombo.setText(`Combo x${this.comboMultiplier.toFixed(1)}`);
this.scene.tweens.add({ targets: this.hudCombo, scale: 1.3, duration: 100, yoyo: true });
}
}
onFall() {
this.combo = 0;
this.comboMultiplier = 1;
this.hudCombo.setText('');
}
showMilestone(blocks) {
const { width, height } = this.scene.scale;
const txt = this.scene.add.text(width / 2, height * 0.35, `EPOCH ${blocks} REACHED!`, {
fontFamily: '"Press Start 2P", monospace',
fontSize: '18px',
color: '#ffd700',
}).setOrigin(0.5).setScrollFactor(0).setDepth(200);
this.scene.tweens.add({
targets: txt,
scale: 1.4,
alpha: 0,
duration: 1500,
onComplete: () => txt.destroy(),
});
}
updateBestDisplay() {
const raw = localStorage.getItem('naddie_best_score');
const best = raw ? (parseInt(raw, 10) || 0) : 0;
this.hudBest.setText(`Best: ${best}`);
}
saveBest() {
const raw = localStorage.getItem('naddie_best_score');
const best = raw ? (parseInt(raw, 10) || 0) : 0;
if (this.score > best) {
localStorage.setItem('naddie_best_score', String(this.score));
return true;
}
return false;
}
destroy() {
this.hudBg.destroy();
this.hudScore.destroy();
this.hudBlocks.destroy();
this.hudCombo.destroy();
this.hudBest.destroy();
}
}

122
src/scenes/BootScene.js Normal file
View File

@@ -0,0 +1,122 @@
import { Scene } from 'phaser';
export class BootScene extends Scene {
constructor() {
super({ key: 'BootScene' });
}
preload() {
// New individual sprites
this.load.image('player_idle', 'assets/sprites/player_idle.png');
this.load.image('player_dead', 'assets/sprites/player_dead.png');
this.load.image('player_propeller', 'assets/sprites/player_propeller.png');
this.load.image('propeller_hat', 'assets/sprites/propeller_hat.png');
this.load.image('rocket', 'assets/sprites/rocket.png');
this.load.image('player_rocket', 'assets/sprites/player_rocket.png');
this.load.image('enemy_bug', 'assets/sprites/enemy_bug.png');
}
create() {
// Generate procedural textures
this.createBackgrounds();
this.createParticles();
this.scene.start('MenuScene');
}
createBackgrounds() {
const gridW = 512;
const gridH = 512;
const gridGraphics = this.make.graphics({ x: 0, y: 0, add: false });
// Deep purple background
gridGraphics.fillStyle(0x0f001f, 1);
gridGraphics.fillRect(0, 0, gridW, gridH);
// Grid lines
gridGraphics.lineStyle(1, 0x2e0059, 0.35);
for (let i = 0; i <= gridW; i += 32) {
gridGraphics.moveTo(i, 0);
gridGraphics.lineTo(i, gridH);
}
for (let i = 0; i <= gridH; i += 32) {
gridGraphics.moveTo(0, i);
gridGraphics.lineTo(gridW, i);
}
gridGraphics.strokePath();
// Some accent hexagons / blockchain nodes
gridGraphics.lineStyle(1, 0x581c87, 0.2);
for (let i = 0; i < 8; i++) {
const hx = Phaser.Math.Between(20, gridW - 20);
const hy = Phaser.Math.Between(20, gridH - 20);
const size = Phaser.Math.Between(6, 14);
gridGraphics.strokeCircle(hx, hy, size);
}
gridGraphics.generateTexture('gridBg', gridW, gridH);
// Star / particle texture
const sGfx = this.make.graphics({ x: 0, y: 0, add: false });
sGfx.fillStyle(0xffffff, 0.9);
sGfx.fillCircle(2, 2, 2);
sGfx.generateTexture('star', 4, 4);
// Gwei particle
const gGfx = this.make.graphics({ x: 0, y: 0, add: false });
gGfx.fillStyle(0xa855f7, 1);
gGfx.fillRect(0, 0, 5, 5);
gGfx.generateTexture('gwei', 5, 5);
// Platform texture — cartoon 3D block inspired by concept art
const pw = 110;
const ph = 32;
const pGfx = this.make.graphics({ x: 0, y: 0, add: false });
// Drop shadow
pGfx.fillStyle(0x000000, 0.25);
pGfx.fillRoundedRect(3, 4, pw, ph, 10);
// Bottom / side face (darker purple for 3D look)
pGfx.fillStyle(0x5b21b6, 1);
pGfx.fillRoundedRect(0, 4, pw, ph - 4, 10);
// Top face (main purple)
pGfx.fillStyle(0x8b5cf6, 1);
pGfx.fillRoundedRect(0, 0, pw, ph - 6, 10);
// Thick cartoon outline
pGfx.lineStyle(3, 0x1e1b4b, 1);
pGfx.strokeRoundedRect(0, 0, pw, ph - 6, 10);
// Highlight on top edge
pGfx.fillStyle(0xffffff, 0.35);
pGfx.fillRoundedRect(5, 2, pw - 10, 6, 4);
// Subtle edge line separating top and side faces
pGfx.lineStyle(2, 0x6d28d9, 0.8);
pGfx.beginPath();
pGfx.moveTo(2, ph - 6);
pGfx.lineTo(pw - 2, ph - 6);
pGfx.strokePath();
pGfx.generateTexture('platform', pw, ph);
// Spring texture (procedural)
const springGfx = this.make.graphics({ x: 0, y: 0, add: false });
springGfx.lineStyle(3, 0xffd700, 1);
springGfx.beginPath();
for (let i = 0; i < 5; i++) {
springGfx.moveTo(5, i * 6);
springGfx.lineTo(15, i * 6 + 3);
springGfx.moveTo(15, i * 6 + 3);
springGfx.lineTo(5, i * 6 + 6);
}
springGfx.strokePath();
springGfx.generateTexture('spring', 20, 32);
}
createParticles() {
// Pre-create particle emitters config if needed
}
}

View File

@@ -0,0 +1,86 @@
import { Scene } from 'phaser';
export class GameOverScene extends Scene {
constructor() {
super({ key: 'GameOverScene' });
}
init(data) {
this.finalScore = data.score || 0;
this.blockHeight = data.blockHeight || 0;
this.isNewBest = data.isNewBest || false;
}
create() {
const { width, height } = this.scale;
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);
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', {
fontFamily: '"Press Start 2P", monospace',
fontSize: '32px',
color: '#ef4444',
align: 'center',
}).setOrigin(0.5).setShadow(3, 3, '#7f1d1d', 0, false, true);
if (this.isNewBest) {
this.add.text(width / 2, height * 0.46, 'NEW BEST!', {
fontFamily: '"Press Start 2P", monospace',
fontSize: '14px',
color: '#22c55e',
}).setOrigin(0.5);
}
this.add.text(width / 2, height * 0.54, `Block Height: ${this.blockHeight}`, {
fontFamily: '"Press Start 2P", monospace',
fontSize: '13px',
color: '#d8b4fe',
}).setOrigin(0.5);
this.add.text(width / 2, height * 0.61, `Gas Score: ${this.finalScore}`, {
fontFamily: '"Press Start 2P", monospace',
fontSize: '16px',
color: '#a855f7',
}).setOrigin(0.5);
this.createButton(width / 2, height * 0.72, 'RETRY', () => {
this.scene.start('GameScene');
});
this.createButton(width / 2, height * 0.82, 'MAIN MENU', () => {
this.scene.start('MenuScene');
});
const submitBtn = this.createButton(width / 2, height * 0.92, 'SUBMIT TO CHAIN', () => {});
submitBtn.bg.setAlpha(0.4);
submitBtn.label.setAlpha(0.4);
}
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 };
}
}

409
src/scenes/GameScene.js Normal file
View File

@@ -0,0 +1,409 @@
import { Scene } from 'phaser';
import { Player } from '../entities/Player.js';
import { Platform } from '../entities/Platform.js';
import { PlatformManager } from '../managers/PlatformManager.js';
import { ScoreManager } from '../managers/ScoreManager.js';
import { GAME_WIDTH, GAME_HEIGHT, GRAVITY } from '../config/game.config.js';
export class GameScene extends Scene {
constructor() {
super({ key: 'GameScene' });
}
create() {
this.physics.world.gravity.y = GRAVITY;
// Background
this.bg = this.add.tileSprite(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 'gridBg')
.setScrollFactor(0)
.setDepth(-10);
// Gwei particles
this.createGweiParticles();
// Input
this.cursors = this.input.keyboard.createCursorKeys();
this.wasd = this.input.keyboard.addKeys({
up: 'W', down: 'S', left: 'A', right: 'D',
});
this.touchLeft = false;
this.touchRight = false;
this.setupTouchControls();
// Start platform
const startY = GAME_HEIGHT - 80;
this.startPlatform = new Platform(this, GAME_WIDTH / 2, startY, 'stable');
this.startPlatform.setTint(0xa78bfa);
this.startPlatform.setAlpha(1);
// Player
this.player = new Player(this, GAME_WIDTH / 2, startY - 140);
this.lastJumpY = this.player.y;
// Player shadow/glow
this.playerShadow = this.add.graphics();
this.playerShadow.setDepth(-4);
// Trail container for fast movement
this.trailPoints = [];
// Managers
this.platformManager = new PlatformManager(this);
this.scoreManager = new ScoreManager(this);
// Pre-spawn platforms
for (let i = 0; i < 10; i++) {
this.platformManager.spawnPlatform(0);
}
// Collisions
this.physics.add.collider(this.player, this.startPlatform, this.handlePlatformCollision, this.platformCollisionFilter, this);
this.physics.add.collider(this.player, this.platformManager.getPlatforms(), this.handlePlatformCollision, this.platformCollisionFilter, this);
this.physics.add.overlap(this.player, this.platformManager.getPowerups(), this.handlePowerup, null, this);
this.physics.add.overlap(this.player, this.platformManager.getEnemies(), this.handleEnemy, null, this);
// Camera
this.cameras.main.setBounds(0, -999999, GAME_WIDTH, 999999 + GAME_HEIGHT);
this.cameras.main.startFollow(this.player, true, 0, 0.05, 0, 180);
// Debug hitbox renderer
this.debugGraphics = this.add.graphics().setDepth(1000);
// Death vignette overlay
this.deathOverlay = this.add.graphics();
this.deathOverlay.setScrollFactor(0);
this.deathOverlay.setDepth(100);
this.deathOverlay.setAlpha(0);
this.isGameOver = false;
this.difficultyLevel = 0;
this.minScrollY = this.cameras.main.scrollY;
}
update(time, delta) {
if (this.isGameOver) return;
this.player.update(this.cursors, this.wasd, this.touchLeft, this.touchRight, time, delta);
// Parallax background
this.bg.tilePositionY = this.cameras.main.scrollY * 0.3;
// Kill line rises with camera but never falls back down
this.minScrollY = Math.min(this.minScrollY, this.cameras.main.scrollY);
const killLine = this.minScrollY + GAME_HEIGHT;
// Update difficulty
const height = Math.max(0, GAME_HEIGHT - this.player.y);
this.difficultyLevel = height;
this.platformManager.update(this.difficultyLevel, killLine);
this.scoreManager.update(this.player.y);
// Check death — same fixed kill line as platform cleanup
if (this.player.body.bottom > killLine) {
this.gameOver();
}
// Reset combo if falling too far without landing
if (this.player.body.velocity.y > 0 && this.player.y > this.lastJumpY + 300) {
this.scoreManager.onFall();
this.lastJumpY = this.player.y;
}
// Update player shadow
this.updatePlayerShadow();
// Update trail for fast movement
this.updateTrail();
// Debug disabled
// this.drawDebug(killLine);
}
updatePlayerShadow() {
this.playerShadow.clear();
if (!this.player.active) return;
const alpha = Math.max(0.05, 0.25 - (Math.abs(this.player.body.velocity.y) / 1200));
this.playerShadow.fillStyle(0x000000, alpha);
this.playerShadow.fillCircle(this.player.x, this.player.y + 42, 16);
}
updateTrail() {
if (!this.player.active) return;
const isFast = this.player.state === 'rocket' || this.player.state === 'propeller';
if (isFast && Math.abs(this.player.body.velocity.y) > 200) {
this.trailPoints.push({ x: this.player.x, y: this.player.y, alpha: 0.5, scale: 1 });
if (this.trailPoints.length > 15) this.trailPoints.shift();
}
for (let i = this.trailPoints.length - 1; i >= 0; i--) {
const point = this.trailPoints[i];
point.alpha -= 0.04;
point.scale -= 0.03;
if (point.alpha <= 0 || point.scale <= 0) {
this.trailPoints.splice(i, 1);
}
}
// Redraw trail
this.trailGraphics = this.trailGraphics || this.add.graphics();
this.trailGraphics.clear();
this.trailGraphics.setDepth(-3);
for (const point of this.trailPoints) {
const color = this.player.state === 'rocket' ? 0xff4444 : 0x44aaff;
this.trailGraphics.fillStyle(color, point.alpha);
this.trailGraphics.fillCircle(point.x, point.y, 8 * point.scale);
}
}
platformCollisionFilter(player, platform) {
if (!platform || !platform.body) return false;
if (typeof platform.isBroken === 'function' && platform.isBroken()) return false;
// Only bounce when falling and player is above platform center
return player.body.velocity.y > 0 && player.y < platform.y + 8;
}
handlePlatformCollision(player, platform) {
if (this.isGameOver) return;
if (platform && typeof platform.onPlayerLand === 'function') {
platform.onPlayerLand(player);
this.scoreManager.onLand(platform.y, platform.platformType || 'stable');
}
this.lastJumpY = player.y;
player.jump();
this.createJumpParticles(player.x, player.y + player.displayHeight / 2 + 3);
}
handlePowerup(player, powerup) {
if (this.isGameOver) return;
if (powerup && typeof powerup.onPlayerTouch === 'function') {
const px = powerup.x;
const py = powerup.y;
powerup.onPlayerTouch(player);
this.createPowerupParticles(px, py);
this.flashScreen();
}
}
handleEnemy(player, enemy) {
if (this.isGameOver) return;
if (player.state === 'rocket') {
this.createExplosion(enemy.x, enemy.y);
enemy.destroy();
return;
}
if (player.state === 'propeller') {
player.endPowerUp();
this.createExplosion(enemy.x, enemy.y);
enemy.destroy();
return;
}
// Mario-style stomp: falling onto enemy kills it
const stompTolerance = 12;
if (player.body.velocity.y > 0 && player.body.bottom <= enemy.body.top + stompTolerance) {
this.createExplosion(enemy.x, enemy.y);
enemy.destroy();
player.jump();
this.scoreManager.score += 25;
this.scoreManager.hudScore.setText(`Gas: ${this.scoreManager.score}`);
this.createJumpParticles(player.x, player.y + player.displayHeight / 2 + 3);
return;
}
this.gameOver(enemy.x, enemy.y);
}
gameOver(ex, ey) {
if (this.isGameOver) return;
this.isGameOver = true;
this.player.die();
this.cameras.main.shake(300, 0.012);
// Small explosion at impact point instead of full-screen vignette
const bx = ex ?? this.player.x;
const by = ey ?? this.player.y;
this.createExplosion(bx, by);
const isNewBest = this.scoreManager.saveBest();
const score = this.scoreManager.score;
const blockHeight = this.scoreManager.blockHeight;
this.time.delayedCall(1500, () => {
this.scoreManager.destroy();
this.scene.start('GameOverScene', { score, blockHeight, isNewBest });
});
}
drawDebug(camBottom) {
this.debugGraphics.clear();
// Kill line — magenta
this.debugGraphics.lineStyle(2, 0xff00ff, 0.7);
this.debugGraphics.lineBetween(0, camBottom, GAME_WIDTH, camBottom);
// Player — red
if (this.player.active && this.player.body) {
this.debugGraphics.lineStyle(2, 0xff0000, 1);
this.debugGraphics.strokeRectShape(this.player.body);
}
// Start platform — green
if (this.startPlatform && this.startPlatform.body) {
this.debugGraphics.lineStyle(2, 0x00ff00, 1);
this.debugGraphics.strokeRectShape(this.startPlatform.body);
}
// Platforms — green
this.platformManager.getPlatforms().children.iterate((p) => {
if (p && p.body) {
this.debugGraphics.lineStyle(2, 0x00ff00, 1);
this.debugGraphics.strokeRectShape(p.body);
}
});
// Enemies — blue
this.platformManager.getEnemies().children.iterate((e) => {
if (e && e.body) {
this.debugGraphics.lineStyle(2, 0x0000ff, 1);
this.debugGraphics.strokeRectShape(e.body);
}
});
// Powerups — yellow
this.platformManager.getPowerups().children.iterate((pu) => {
if (pu && pu.body) {
this.debugGraphics.lineStyle(2, 0xffff00, 1);
this.debugGraphics.strokeRectShape(pu.body);
}
});
}
showDeathVignette() {
const cx = GAME_WIDTH / 2;
const cy = GAME_HEIGHT / 2;
const maxR = Math.max(GAME_WIDTH, GAME_HEIGHT);
this.deathOverlay.clear();
// Draw a radial gradient-like vignette using fewer circles
const steps = 5;
const band = (maxR * 0.8) / steps;
for (let i = steps; i >= 0; i--) {
const t = i / steps;
const r = 50 + t * (maxR * 0.8);
const alpha = 0.05 + (1 - t) * 0.55;
this.deathOverlay.lineStyle(band, 0x7f0000, alpha);
this.deathOverlay.strokeCircle(cx, cy, r);
}
this.deathOverlay.setAlpha(0);
this.tweens.add({
targets: this.deathOverlay,
alpha: 1,
duration: 400,
ease: 'Quad.easeOut',
});
}
flashScreen() {
const flash = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0xffffff, 0.3);
flash.setScrollFactor(0);
flash.setDepth(90);
this.tweens.add({
targets: flash,
alpha: 0,
duration: 250,
onComplete: () => flash.destroy(),
});
}
setupTouchControls() {
const { width, height } = this.scale;
this.input.on('pointerdown', (pointer) => {
if (pointer.x < width / 2) this.touchLeft = true;
else this.touchRight = true;
});
this.input.on('pointerup', () => {
this.touchLeft = false;
this.touchRight = false;
});
this.input.on('pointermove', (pointer) => {
if (pointer.isDown) {
this.touchLeft = pointer.x < width / 2;
this.touchRight = pointer.x >= width / 2;
}
});
}
createGweiParticles() {
for (let i = 0; i < 25; i++) {
const p = this.add.image(
Phaser.Math.Between(0, GAME_WIDTH),
Phaser.Math.Between(0, GAME_HEIGHT),
'gwei'
).setAlpha(0.5);
const scene = this;
this.tweens.add({
targets: p,
y: p.y - Phaser.Math.Between(100, 400),
alpha: 0,
duration: Phaser.Math.Between(3000, 7000),
repeat: -1,
delay: Phaser.Math.Between(0, 4000),
onRepeat: function() {
p.y = scene.cameras.main.scrollY + GAME_HEIGHT + 20;
p.x = Phaser.Math.Between(0, GAME_WIDTH);
p.setAlpha(0.5);
},
});
}
}
createJumpParticles(x, y) {
for (let i = 0; i < 8; i++) {
const size = Phaser.Math.Between(4, 7);
const p = this.add.rectangle(x, y, size, size, 0xa855f7).setAlpha(0.9);
this.tweens.add({
targets: p,
x: x + Phaser.Math.Between(-35, 35),
y: y + Phaser.Math.Between(10, 45),
alpha: 0,
scale: 0,
angle: Phaser.Math.Between(-90, 90),
duration: 250,
onComplete: () => p.destroy(),
});
}
}
createPowerupParticles(x, y) {
for (let i = 0; i < 10; i++) {
const color = [0xffd700, 0xa855f7, 0x22c55e][Phaser.Math.Between(0, 2)];
const p = this.add.rectangle(x, y, 5, 5, color).setAlpha(1);
this.tweens.add({
targets: p,
x: x + Phaser.Math.Between(-50, 50),
y: y + Phaser.Math.Between(-50, 50),
alpha: 0,
scale: 0,
duration: 600,
onComplete: () => p.destroy(),
});
}
}
createExplosion(x, y) {
for (let i = 0; i < 12; i++) {
const p = this.add.rectangle(x, y, 6, 6, 0xef4444).setAlpha(1);
const angle = Phaser.Math.Between(0, 360);
const dist = Phaser.Math.Between(30, 80);
this.tweens.add({
targets: p,
x: x + Math.cos(angle * Math.PI / 180) * dist,
y: y + Math.sin(angle * Math.PI / 180) * dist,
alpha: 0,
scale: 0,
duration: 500,
onComplete: () => p.destroy(),
});
}
}
}

164
src/scenes/MenuScene.js Normal file
View File

@@ -0,0 +1,164 @@
import { Scene } from 'phaser';
export class MenuScene extends Scene {
constructor() {
super({ key: 'MenuScene' });
}
create() {
const { width, height } = this.scale;
// Background
this.add.tileSprite(width / 2, height / 2, width, height, 'gridBg');
// Title
this.add.text(width / 2, height * 0.18, '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', {
fontFamily: '"Press Start 2P", monospace',
fontSize: '14px',
color: '#a855f7',
align: 'center',
}).setOrigin(0.5);
// Floating Naddie preview
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',
});
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.scene.start('GameScene');
});
this.add.text(width / 2, height * 0.74, 'Press ENTER or SPACE to start', {
fontFamily: '"Press Start 2P", monospace',
fontSize: '10px',
color: '#888',
align: 'center',
}).setOrigin(0.5);
this.input.keyboard.on('keydown-ENTER', () => {
this.scene.start('GameScene');
});
this.input.keyboard.on('keydown-SPACE', () => {
this.scene.start('GameScene');
});
// Leaderboard button
this.createButton(width / 2, height * 0.78, 'LEADERBOARD', () => {
this.showLeaderboard();
});
this.add.text(width / 2, height * 0.92, 'Web3 integration coming soon', {
fontFamily: '"Press Start 2P", monospace',
fontSize: '9px',
color: '#444',
align: 'center',
}).setOrigin(0.5);
this.createAmbientParticles();
}
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', callback);
return { bg, label };
}
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: parseInt(localStorage.getItem('naddie_best_score') || '0', 10) },
];
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', () => {
overlay.destroy();
panel.destroy();
title.destroy();
close.destroy();
rows.forEach(r => { r.rank.destroy(); r.addr.destroy(); r.score.destroy(); });
});
}
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),
});
}
}
}