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:
@@ -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
62
src/entities/Coin.js
Normal 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
33
src/entities/Magnet.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
33
src/entities/Shield.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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_',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user