New mechanics: coins + magnet + Gas Limit shield

Coins: collectible  dropped in clusters (lines/arcs) above platforms; collected on overlap, banked into a persistent wallet at game over, shown in HUD and on the GameOver screen. Magnet power-up pulls nearby coins toward the player for 6s (HUD duration bar). Gas Limit shield power-up grants a cyan aura that absorbs one enemy hit instead of dying, then breaks. New procedural textures (coin/magnet/shield), spawn weights, durations, sound, and stats (coins collected, GWEI wallet) wired through ScoreManager/StatsManager/Settings. Verified via in-browser sim: coins spawn, magnet pull 100px to 10px, collect increments GWEI, shield absorbs a hit (no game over) while no shield = death.
This commit is contained in:
2026-06-01 03:01:01 +07:00
parent 7c6a792212
commit be1933f3ba
14 changed files with 366 additions and 11 deletions

View File

@@ -24,6 +24,15 @@ export const POWERUP_RATES = {
spring: 0.04, spring: 0.04,
propeller: 0.025, propeller: 0.025,
rocket: 0.012, rocket: 0.012,
magnet: 0.02,
shield: 0.025,
};
export const COIN = {
spawnChance: 0.42, // per platform, chance to drop a cluster of coins
value: 1, // $GWEI per coin
magnetRadius: 150, // px within which the magnet pulls coins
magnetSpeed: 560, // px/s pull speed
}; };
export const ENEMY_RATES = { export const ENEMY_RATES = {
@@ -65,4 +74,5 @@ export const PHYSICS = {
export const POWERUP_DURATION = { export const POWERUP_DURATION = {
propeller: 3500, propeller: 3500,
rocket: 3000, rocket: 3000,
magnet: 6000,
}; };

62
src/entities/Coin.js Normal file
View File

@@ -0,0 +1,62 @@
import { Physics } from 'phaser';
import { COIN } from '../config/game.config.js';
export class Coin extends Physics.Arcade.Sprite {
constructor(scene, x, y) {
super(scene, x, y, 'coin');
scene.add.existing(this);
scene.physics.add.existing(this);
this.setScale(0.7);
this.body.allowGravity = false;
this.body.setCircle(this.width / 2);
this.collected = false;
// subtle idle pulse
this.pulse = scene.tweens.add({
targets: this,
scaleX: 0.78,
scaleY: 0.78,
duration: 600,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
});
}
preUpdate(time, delta) {
super.preUpdate(time, delta);
if (this.collected) return;
const p = this.scene.player;
if (p && p.active && p.magnetActive) {
const dist = Phaser.Math.Distance.Between(this.x, this.y, p.x, p.y);
if (dist < COIN.magnetRadius) {
const ang = Phaser.Math.Angle.Between(this.x, this.y, p.x, p.y);
const step = COIN.magnetSpeed * (delta / 1000);
this.x += Math.cos(ang) * step;
this.y += Math.sin(ang) * step;
}
}
}
collect() {
if (this.collected) return false;
this.collected = true;
if (this.pulse) { this.pulse.stop(); this.pulse = null; }
if (this.body) this.body.enable = false;
this.scene.tweens.add({
targets: this,
scale: 1.4,
alpha: 0,
duration: 160,
ease: 'Quad.easeOut',
onComplete: () => this.destroy(),
});
return true;
}
destroy(fromScene) {
if (this.pulse) { this.pulse.stop(); this.pulse = null; }
super.destroy(fromScene);
}
}

33
src/entities/Magnet.js Normal file
View File

@@ -0,0 +1,33 @@
import { Physics } from 'phaser';
export class Magnet extends Physics.Arcade.Sprite {
constructor(scene, x, y) {
super(scene, x, y, 'magnet');
scene.add.existing(this);
scene.physics.add.existing(this, true);
this.setScale(0.8);
this.body.setSize(this.width * 0.8, this.height * 0.8);
this.body.setOffset(this.width * 0.1, this.height * 0.1);
this.consumed = false;
this.floatTween = scene.tweens.add({
targets: this, y: y - 6, duration: 700, yoyo: true, repeat: -1, ease: 'Sine.easeInOut',
});
}
onPlayerTouch(player) {
if (this.consumed) return false;
this.consumed = true;
this.body.enable = false;
this.setVisible(false);
player.startMagnet();
this.destroy();
return true;
}
destroy(fromScene) {
if (this.floatTween) { this.floatTween.stop(); this.floatTween = null; }
super.destroy(fromScene);
}
}

View File

@@ -21,11 +21,28 @@ export class Player extends Physics.Arcade.Sprite {
this.state = 'normal'; this.state = 'normal';
this.propellerTimer = 0; this.propellerTimer = 0;
this.rocketTimer = 0; this.rocketTimer = 0;
// Magnet and shield are independent of the movement state machine.
this.magnetTimer = 0;
this.magnetActive = false;
this.shielded = false;
this.shieldAura = null;
} }
update(cursors, wasd, touchLeft, touchRight, time, delta) { update(cursors, wasd, touchLeft, touchRight, time, delta) {
if (this.state === 'dead') return; if (this.state === 'dead') return;
// Magnet ticks in every state.
if (this.magnetTimer > 0) {
this.magnetTimer -= delta;
this.magnetActive = this.magnetTimer > 0;
}
// Keep the shield aura glued to the player.
if (this.shieldAura) {
this.shieldAura.setPosition(this.x, this.y);
}
let velocityX = 0; let velocityX = 0;
if (cursors.left.isDown || wasd.left.isDown || touchLeft) velocityX = -PLAYER_SPEED; if (cursors.left.isDown || wasd.left.isDown || touchLeft) velocityX = -PLAYER_SPEED;
if (cursors.right.isDown || wasd.right.isDown || touchRight) velocityX = PLAYER_SPEED; if (cursors.right.isDown || wasd.right.isDown || touchRight) velocityX = PLAYER_SPEED;
@@ -94,6 +111,46 @@ export class Player extends Physics.Arcade.Sprite {
this.setScale(0.45); this.setScale(0.45);
} }
startMagnet() {
this.magnetTimer = POWERUP_DURATION.magnet;
this.magnetActive = true;
}
startShield() {
this.shielded = true;
if (this.shieldAura) this.shieldAura.destroy();
this.shieldAura = this.scene.add.image(this.x, this.y, 'px_ring')
.setTint(0x22d3ee)
.setScale(1.5)
.setAlpha(0.7)
.setDepth(this.depth - 1);
this.scene.tweens.add({
targets: this.shieldAura,
scale: 1.7,
alpha: 0.35,
duration: 700,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
});
}
consumeShield() {
this.shielded = false;
if (this.shieldAura) {
const aura = this.shieldAura;
this.shieldAura = null;
this.scene.tweens.add({
targets: aura,
scale: 2.6,
alpha: 0,
duration: 250,
ease: 'Quad.easeOut',
onComplete: () => aura.destroy(),
});
}
}
startPropeller() { startPropeller() {
if (this.state === 'dead') return; if (this.state === 'dead') return;
this._stopSquash(); this._stopSquash();
@@ -132,6 +189,10 @@ export class Player extends Physics.Arcade.Sprite {
die() { die() {
if (this.state === 'dead') return; if (this.state === 'dead') return;
this._stopSquash(); this._stopSquash();
this.magnetActive = false;
this.magnetTimer = 0;
if (this.shieldAura) { this.shieldAura.destroy(); this.shieldAura = null; }
this.shielded = false;
this.state = 'dead'; this.state = 'dead';
this.setTexture('player_dead'); this.setTexture('player_dead');
this.setScale(0.4); this.setScale(0.4);

33
src/entities/Shield.js Normal file
View File

@@ -0,0 +1,33 @@
import { Physics } from 'phaser';
export class Shield extends Physics.Arcade.Sprite {
constructor(scene, x, y) {
super(scene, x, y, 'shield');
scene.add.existing(this);
scene.physics.add.existing(this, true);
this.setScale(0.8);
this.body.setSize(this.width * 0.8, this.height * 0.8);
this.body.setOffset(this.width * 0.1, this.height * 0.1);
this.consumed = false;
this.floatTween = scene.tweens.add({
targets: this, y: y - 6, duration: 700, yoyo: true, repeat: -1, ease: 'Sine.easeInOut',
});
}
onPlayerTouch(player) {
if (this.consumed) return false;
this.consumed = true;
this.body.enable = false;
this.setVisible(false);
player.startShield();
this.destroy();
return true;
}
destroy(fromScene) {
if (this.floatTween) { this.floatTween.stop(); this.floatTween = null; }
super.destroy(fromScene);
}
}

View File

@@ -2,11 +2,14 @@ import { Platform } from '../entities/Platform.js';
import { Spring } from '../entities/Spring.js'; import { Spring } from '../entities/Spring.js';
import { PropellerHat } from '../entities/PropellerHat.js'; import { PropellerHat } from '../entities/PropellerHat.js';
import { Rocket } from '../entities/Rocket.js'; import { Rocket } from '../entities/Rocket.js';
import { Magnet } from '../entities/Magnet.js';
import { Shield } from '../entities/Shield.js';
import { Coin } from '../entities/Coin.js';
import { Enemy } from '../entities/Enemy.js'; import { Enemy } from '../entities/Enemy.js';
import { rng } from '../utils/random.js'; import { rng } from '../utils/random.js';
import { import {
GAME_WIDTH, PLATFORM_GAP_MIN, PLATFORM_GAP_MAX, GAME_WIDTH, PLATFORM_GAP_MIN, PLATFORM_GAP_MAX,
SPAWN_RATES, POWERUP_RATES, ENEMY_RATES, DIFFICULTY, UNLOCK, SPAWN_RATES, POWERUP_RATES, ENEMY_RATES, DIFFICULTY, UNLOCK, COIN,
} from '../config/game.config.js'; } from '../config/game.config.js';
export class PlatformManager { export class PlatformManager {
@@ -15,6 +18,7 @@ export class PlatformManager {
this.platforms = scene.add.group({ classType: Platform }); this.platforms = scene.add.group({ classType: Platform });
this.enemies = scene.add.group({ classType: Enemy }); this.enemies = scene.add.group({ classType: Enemy });
this.powerups = scene.add.group(); this.powerups = scene.add.group();
this.coins = scene.add.group({ classType: Coin });
this.lastY = scene.scale.height - 80; this.lastY = scene.scale.height - 80;
this.highestY = this.lastY; this.highestY = this.lastY;
} }
@@ -29,6 +33,7 @@ export class PlatformManager {
this.cleanupGroup(this.platforms, killLine); this.cleanupGroup(this.platforms, killLine);
this.cleanupGroup(this.enemies, killLine); this.cleanupGroup(this.enemies, killLine);
this.cleanupGroup(this.powerups, killLine); this.cleanupGroup(this.powerups, killLine);
this.cleanupGroup(this.coins, killLine);
} }
cleanupGroup(group, killLine) { cleanupGroup(group, killLine) {
@@ -71,15 +76,20 @@ export class PlatformManager {
if (type !== 'breaking' && type !== 'reorg') { if (type !== 'breaking' && type !== 'reorg') {
this.maybeSpawnPowerUp(x, this.highestY - 25); this.maybeSpawnPowerUp(x, this.highestY - 25);
} }
this.maybeSpawnCoins(x, this.highestY);
this.maybeSpawnEnemy(x, this.highestY, difficultyLevel); this.maybeSpawnEnemy(x, this.highestY, difficultyLevel);
} }
maybeSpawnPowerUp(platformX, y) { maybeSpawnPowerUp(platformX, y) {
const r = POWERUP_RATES;
const rand = rng.frac(); const rand = rng.frac();
let type = null; let type = null;
if (rand < POWERUP_RATES.spring) type = 'spring'; let acc = 0;
else if (rand < POWERUP_RATES.spring + POWERUP_RATES.propeller) type = 'propeller'; if (rand < (acc += r.spring)) type = 'spring';
else if (rand < POWERUP_RATES.spring + POWERUP_RATES.propeller + POWERUP_RATES.rocket) type = 'rocket'; else if (rand < (acc += r.propeller)) type = 'propeller';
else if (rand < (acc += r.rocket)) type = 'rocket';
else if (rand < (acc += r.magnet)) type = 'magnet';
else if (rand < (acc += r.shield)) type = 'shield';
if (!type) return; if (!type) return;
@@ -90,6 +100,32 @@ export class PlatformManager {
this.powerups.add(new PropellerHat(this.scene, x, y - 35)); this.powerups.add(new PropellerHat(this.scene, x, y - 35));
} else if (type === 'rocket') { } else if (type === 'rocket') {
this.powerups.add(new Rocket(this.scene, x, y - 45)); this.powerups.add(new Rocket(this.scene, x, y - 45));
} else if (type === 'magnet') {
this.powerups.add(new Magnet(this.scene, x, y - 35));
} else if (type === 'shield') {
this.powerups.add(new Shield(this.scene, x, y - 35));
}
}
// Drop a small cluster of $GWEI coins (line or arc) above a platform.
maybeSpawnCoins(platformX, platformY) {
if (rng.frac() >= COIN.spawnChance) return;
const count = rng.between(3, 5);
const spacing = 26;
const arc = rng.frac() < 0.5; // half arcs, half vertical lines
const baseX = Phaser.Math.Clamp(platformX + rng.between(-30, 30), 40, GAME_WIDTH - 40);
const topY = platformY - rng.between(40, 70);
for (let i = 0; i < count; i++) {
let cx = baseX;
let cy = topY - i * spacing;
if (arc) {
cx = baseX + Math.sin((i / (count - 1)) * Math.PI) * 36 * (rng.frac() < 0.5 ? 1 : -1);
cy = topY - i * (spacing - 4);
}
cx = Phaser.Math.Clamp(cx, 20, GAME_WIDTH - 20);
this.coins.add(new Coin(this.scene, cx, cy));
} }
} }
@@ -143,4 +179,8 @@ export class PlatformManager {
getPowerups() { getPowerups() {
return this.powerups; return this.powerups;
} }
getCoins() {
return this.coins;
}
} }

View File

@@ -1,4 +1,4 @@
import { SCORE } from '../config/game.config.js'; import { SCORE, POWERUP_DURATION } from '../config/game.config.js';
import { storage, KEYS } from '../utils/storage.js'; import { storage, KEYS } from '../utils/storage.js';
export class ScoreManager { export class ScoreManager {
@@ -10,6 +10,7 @@ export class ScoreManager {
this.comboMultiplier = 1; this.comboMultiplier = 1;
this.genesisActive = false; this.genesisActive = false;
this.genesisJumps = 0; this.genesisJumps = 0;
this.gwei = 0;
this.hudBg = scene.add.rectangle(10, 10, 200, 90, 0x000000, 0.5) this.hudBg = scene.add.rectangle(10, 10, 200, 90, 0x000000, 0.5)
.setOrigin(0) .setOrigin(0)
@@ -40,6 +41,12 @@ 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);
this.hudGwei = scene.add.text(scene.scale.width - 16, 34, '◈ 0', {
fontFamily: '"Press Start 2P", monospace',
fontSize: '12px',
color: '#ffd700',
}).setOrigin(1, 0).setScrollFactor(0).setDepth(200).setShadow(1, 1, '#000000', 2, false, true);
// Powerup duration bar (hidden until active) // Powerup duration bar (hidden until active)
this.powerupBarBg = scene.add.rectangle(scene.scale.width / 2, scene.scale.height - 24, 200, 12, 0x000000, 0.6) this.powerupBarBg = scene.add.rectangle(scene.scale.width / 2, scene.scale.height - 24, 200, 12, 0x000000, 0.6)
.setStrokeStyle(2, 0xa855f7) .setStrokeStyle(2, 0xa855f7)
@@ -92,6 +99,12 @@ export class ScoreManager {
total = 3000; total = 3000;
color = 0xff4444; color = 0xff4444;
label = 'ROCKET'; label = 'ROCKET';
} else if (player.magnetActive) {
active = 'magnet';
remaining = Math.max(0, player.magnetTimer);
total = POWERUP_DURATION.magnet;
color = 0xffd700;
label = 'MAGNET';
} }
if (active) { if (active) {
@@ -166,6 +179,12 @@ export class ScoreManager {
if (this.scene.achievements) this.scene.achievements.onScore(this.score); if (this.scene.achievements) this.scene.achievements.onScore(this.score);
} }
addGwei(amount) {
this.gwei += amount;
this.hudGwei.setText(`${this.gwei}`);
this.scene.tweens.add({ targets: this.hudGwei, scale: 1.3, duration: 90, yoyo: true });
}
onFall() { onFall() {
if (this.combo > 0) { if (this.combo > 0) {
this.combo = 0; this.combo = 0;
@@ -214,6 +233,7 @@ export class ScoreManager {
this.hudBlocks.destroy(); this.hudBlocks.destroy();
this.hudCombo.destroy(); this.hudCombo.destroy();
this.hudBest.destroy(); this.hudBest.destroy();
this.hudGwei.destroy();
this.powerupBarBg.destroy(); this.powerupBarBg.destroy();
this.powerupBarFill.destroy(); this.powerupBarFill.destroy();
this.powerupBarLabel.destroy(); this.powerupBarLabel.destroy();

View File

@@ -142,6 +142,11 @@ class SoundManager {
this._beep(660, 0.05, 'square', 0.12); this._beep(660, 0.05, 'square', 0.12);
} }
coin() {
this._beep(988, 0.05, 'square', 0.12);
setTimeout(() => this._beep(1319, 0.08, 'square', 0.12), 45);
}
newBest() { newBest() {
[523, 659, 783, 1046, 1318].forEach((f, i) => setTimeout(() => this._beep(f, 0.18, 'triangle', 0.25), i * 80)); [523, 659, 783, 1046, 1318].forEach((f, i) => setTimeout(() => this._beep(f, 0.18, 'triangle', 0.25), i * 80));
} }

View File

@@ -5,6 +5,7 @@ const DEFAULTS = {
totalJumps: 0, totalJumps: 0,
totalStomps: 0, totalStomps: 0,
totalBlocks: 0, totalBlocks: 0,
totalCoins: 0,
bestCombo: 0, bestCombo: 0,
}; };
@@ -14,7 +15,7 @@ const DEFAULTS = {
*/ */
export class StatsManager { export class StatsManager {
constructor() { constructor() {
this.run = { jumps: 0, stomps: 0, bestCombo: 0 }; this.run = { jumps: 0, stomps: 0, coins: 0, bestCombo: 0 };
} }
static load() { static load() {
@@ -34,6 +35,10 @@ export class StatsManager {
this.run.stomps += 1; this.run.stomps += 1;
} }
onCoin() {
this.run.coins += 1;
}
onCombo(multiplier) { onCombo(multiplier) {
if (multiplier > this.run.bestCombo) this.run.bestCombo = multiplier; if (multiplier > this.run.bestCombo) this.run.bestCombo = multiplier;
} }
@@ -44,6 +49,7 @@ export class StatsManager {
t.gamesPlayed += 1; t.gamesPlayed += 1;
t.totalJumps += this.run.jumps; t.totalJumps += this.run.jumps;
t.totalStomps += this.run.stomps; t.totalStomps += this.run.stomps;
t.totalCoins += this.run.coins;
t.totalBlocks += blockHeight || 0; t.totalBlocks += blockHeight || 0;
t.bestCombo = Math.max(t.bestCombo, this.run.bestCombo); t.bestCombo = Math.max(t.bestCombo, this.run.bestCombo);
storage.setJSON(KEYS.stats, t); storage.setJSON(KEYS.stats, t);

View File

@@ -148,5 +148,52 @@ export class BootScene extends Scene {
} }
springGfx.strokePath(); springGfx.strokePath();
springGfx.generateTexture('spring', 20, 32); springGfx.generateTexture('spring', 20, 32);
// $GWEI coin — gold disc with highlight + purple diamond (matches Naddie)
const coin = this.make.graphics({ x: 0, y: 0, add: false });
coin.fillStyle(0xb8860b, 1); coin.fillCircle(16, 16, 15);
coin.fillStyle(0xffd700, 1); coin.fillCircle(16, 16, 13);
coin.fillStyle(0xfff3b0, 1); coin.fillCircle(16, 16, 9);
coin.fillStyle(0xffd700, 1); coin.fillCircle(16, 16, 7);
coin.fillStyle(0x7c3aed, 1);
coin.beginPath();
coin.moveTo(16, 9); coin.lineTo(21, 16); coin.lineTo(16, 23); coin.lineTo(11, 16);
coin.closePath(); coin.fillPath();
coin.fillStyle(0xffffff, 0.5); coin.fillCircle(12, 11, 2);
coin.generateTexture('coin', 32, 32);
// Magnet — classic red/grey horseshoe
const mag = this.make.graphics({ x: 0, y: 0, add: false });
mag.lineStyle(9, 0xe11d48, 1);
mag.beginPath();
mag.arc(20, 18, 13, Math.PI, 0, false); // top arc opening downward
mag.strokePath();
mag.lineStyle(9, 0xe11d48, 1);
mag.beginPath(); mag.moveTo(7, 18); mag.lineTo(7, 32); mag.strokePath();
mag.beginPath(); mag.moveTo(33, 18); mag.lineTo(33, 32); mag.strokePath();
mag.fillStyle(0xd1d5db, 1);
mag.fillRect(3, 31, 8, 6);
mag.fillRect(29, 31, 8, 6);
mag.generateTexture('magnet', 40, 40);
// Shield — cyan heater shield with glassy fill
const sh = this.make.graphics({ x: 0, y: 0, add: false });
const drawShield = (inset, fill, alpha) => {
sh.fillStyle(fill, alpha);
sh.beginPath();
sh.moveTo(20, 2 + inset);
sh.lineTo(36 - inset, 9 + inset / 2);
sh.lineTo(36 - inset, 22);
sh.lineTo(20, 40 - inset);
sh.lineTo(4 + inset, 22);
sh.lineTo(4 + inset, 9 + inset / 2);
sh.closePath();
sh.fillPath();
};
drawShield(0, 0x0e7490, 1);
drawShield(3, 0x22d3ee, 1);
drawShield(8, 0xa5f3fc, 0.9);
sh.fillStyle(0xffffff, 0.5); sh.fillRect(12, 10, 4, 12);
sh.generateTexture('shield', 40, 42);
} }
} }

View File

@@ -2,6 +2,7 @@ import { Scene } from 'phaser';
import { sound } from '../managers/SoundManager.js'; import { sound } from '../managers/SoundManager.js';
import { createButton } from '../utils/ui.js'; import { createButton } from '../utils/ui.js';
import { todaySeed } from '../utils/random.js'; import { todaySeed } from '../utils/random.js';
import { storage, KEYS } from '../utils/storage.js';
export class GameOverScene extends Scene { export class GameOverScene extends Scene {
constructor() { constructor() {
@@ -14,6 +15,7 @@ export class GameOverScene extends Scene {
this.isNewBest = data.isNewBest || false; this.isNewBest = data.isNewBest || false;
this.mode = data.mode || 'normal'; this.mode = data.mode || 'normal';
this.dailyBest = data.dailyBest; this.dailyBest = data.dailyBest;
this.gwei = data.gwei || 0;
} }
retry() { retry() {
@@ -55,14 +57,21 @@ export class GameOverScene extends Scene {
color: '#d8b4fe', color: '#d8b4fe',
}).setOrigin(0.5); }).setOrigin(0.5);
this.add.text(width / 2, height * 0.61, `Gas Score: ${this.finalScore}`, { this.add.text(width / 2, height * 0.60, `Gas Score: ${this.finalScore}`, {
fontFamily: '"Press Start 2P", monospace', fontFamily: '"Press Start 2P", monospace',
fontSize: '16px', fontSize: '16px',
color: '#a855f7', color: '#a855f7',
}).setOrigin(0.5); }).setOrigin(0.5);
const wallet = storage.getInt(KEYS.gwei, 0);
this.add.text(width / 2, height * 0.655, `◈ +${this.gwei} $GWEI (wallet: ${wallet})`, {
fontFamily: '"Press Start 2P", monospace',
fontSize: '11px',
color: '#ffd700',
}).setOrigin(0.5);
if (this.mode === 'daily') { if (this.mode === 'daily') {
this.add.text(width / 2, height * 0.665, `DAILY · Today's Best: ${this.dailyBest ?? this.finalScore}`, { this.add.text(width / 2, height * 0.695, `DAILY · Today's Best: ${this.dailyBest ?? this.finalScore}`, {
fontFamily: '"Press Start 2P", monospace', fontFamily: '"Press Start 2P", monospace',
fontSize: '10px', fontSize: '10px',
color: '#ffd700', color: '#ffd700',

View File

@@ -10,7 +10,7 @@ import { StatsManager } from '../managers/StatsManager.js';
import { sound } from '../managers/SoundManager.js'; import { sound } from '../managers/SoundManager.js';
import { storage, KEYS } from '../utils/storage.js'; import { storage, KEYS } from '../utils/storage.js';
import { rng } from '../utils/random.js'; import { rng } from '../utils/random.js';
import { GAME_WIDTH, GAME_HEIGHT, SCORE, PHYSICS } from '../config/game.config.js'; import { GAME_WIDTH, GAME_HEIGHT, SCORE, PHYSICS, COIN } from '../config/game.config.js';
export class GameScene extends Scene { export class GameScene extends Scene {
constructor() { constructor() {
@@ -70,6 +70,7 @@ export class GameScene extends Scene {
this.physics.add.collider(this.player, this.platformManager.getPlatforms(), 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.getPowerups(), this.handlePowerup, null, this);
this.physics.add.overlap(this.player, this.platformManager.getEnemies(), this.handleEnemy, null, this); this.physics.add.overlap(this.player, this.platformManager.getEnemies(), this.handleEnemy, null, this);
this.physics.add.overlap(this.player, this.platformManager.getCoins(), this.handleCoin, null, this);
this.cameras.main.setBounds(0, -999999, GAME_WIDTH, 999999 + GAME_HEIGHT); this.cameras.main.setBounds(0, -999999, GAME_WIDTH, 999999 + GAME_HEIGHT);
// Doodle-jump camera: free movement below the trigger line, camera only // Doodle-jump camera: free movement below the trigger line, camera only
@@ -250,9 +251,27 @@ export class GameScene extends Scene {
this.createJumpParticles(player.x, player.y + player.displayHeight / 2 + 3); this.createJumpParticles(player.x, player.y + player.displayHeight / 2 + 3);
return; return;
} }
// Gas Limit shield absorbs one hit instead of dying.
if (player.shielded) {
player.consumeShield();
this.createExplosion(enemy.x, enemy.y);
enemy.destroy();
sound.stomp();
this.flashScreen();
return;
}
this.gameOver(enemy.x, enemy.y); this.gameOver(enemy.x, enemy.y);
} }
handleCoin(player, coin) {
if (this.isGameOver) return;
if (coin && typeof coin.collect === 'function' && coin.collect()) {
this.scoreManager.addGwei(COIN.value);
if (this.stats) this.stats.onCoin();
sound.coin();
}
}
gameOver(ex, ey) { gameOver(ex, ey) {
if (this.isGameOver) return; if (this.isGameOver) return;
this.isGameOver = true; this.isGameOver = true;
@@ -268,6 +287,12 @@ export class GameScene extends Scene {
} }
const score = this.scoreManager.score; const score = this.scoreManager.score;
const blockHeight = this.scoreManager.blockHeight; const blockHeight = this.scoreManager.blockHeight;
const gwei = this.scoreManager.gwei;
// Bank the run's $GWEI into the persistent wallet.
if (gwei > 0) {
storage.setItem(KEYS.gwei, storage.getInt(KEYS.gwei, 0) + gwei);
}
if (this.stats) this.stats.flush(blockHeight); if (this.stats) this.stats.flush(blockHeight);
@@ -285,7 +310,7 @@ export class GameScene extends Scene {
this.time.delayedCall(1500, () => { this.time.delayedCall(1500, () => {
this.scoreManager.destroy(); this.scoreManager.destroy();
this.scene.start('GameOverScene', { this.scene.start('GameOverScene', {
score, blockHeight, isNewBest, mode: this.mode, dailyBest, score, blockHeight, isNewBest, mode: this.mode, dailyBest, gwei,
}); });
}); });
} }

View File

@@ -234,12 +234,14 @@ export class MenuScene extends Scene {
['Games played', s.gamesPlayed], ['Games played', s.gamesPlayed],
['Total jumps', s.totalJumps], ['Total jumps', s.totalJumps],
['Enemies stomped', s.totalStomps], ['Enemies stomped', s.totalStomps],
['Coins collected', s.totalCoins || 0],
['Blocks climbed', s.totalBlocks], ['Blocks climbed', s.totalBlocks],
['Best combo', `x${(s.bestCombo || 1).toFixed(1)}`], ['Best combo', `x${(s.bestCombo || 1).toFixed(1)}`],
['Best score', storage.getInt(KEYS.best, 0)], ['Best score', storage.getInt(KEYS.best, 0)],
['$GWEI wallet', storage.getInt(KEYS.gwei, 0)],
]; ];
rows.forEach((row, i) => { rows.forEach((row, i) => {
const y = height / 2 - 100 + i * 42; const y = height / 2 - 130 + i * 38;
m.add(this.add.text(width / 2 - 170, y, row[0], { m.add(this.add.text(width / 2 - 170, y, row[0], {
fontFamily: '"Press Start 2P", monospace', fontSize: '10px', color: '#d8b4fe', fontFamily: '"Press Start 2P", monospace', fontSize: '10px', color: '#d8b4fe',
}).setOrigin(0, 0.5)); }).setOrigin(0, 0.5));
@@ -300,6 +302,7 @@ export class MenuScene extends Scene {
StatsManager.reset(); StatsManager.reset();
storage.removeItem(KEYS.best); storage.removeItem(KEYS.best);
storage.removeItem(KEYS.achievements); storage.removeItem(KEYS.achievements);
storage.removeItem(KEYS.gwei);
resetBtn.label.setText('DONE'); resetBtn.label.setText('DONE');
sound.click(); sound.click();
}, { width: 240, height: 42, fontSize: '12px', bgColor: 0x581c87, hoverColor: 0x7e22ce }); }, { width: 240, height: 42, fontSize: '12px', bgColor: 0x581c87, hoverColor: 0x7e22ce });

View File

@@ -11,6 +11,7 @@ export const KEYS = {
tutorialSeen: 'naddie_tutorial_seen', tutorialSeen: 'naddie_tutorial_seen',
achievements: 'naddie_achievements_v1', achievements: 'naddie_achievements_v1',
particleQuality: 'naddie_particle_quality', particleQuality: 'naddie_particle_quality',
gwei: 'naddie_gwei_total',
stats: 'naddie_stats_v1', stats: 'naddie_stats_v1',
dailyPrefix: 'naddie_daily_', dailyPrefix: 'naddie_daily_',
}; };