diff --git a/src/entities/Enemy.js b/src/entities/Enemy.js index cbda964..a399119 100644 --- a/src/entities/Enemy.js +++ b/src/entities/Enemy.js @@ -8,29 +8,46 @@ export class Enemy extends Physics.Arcade.Sprite { 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.setScale(0.7); + this.body.setSize(this.width * 0.7, this.height * 0.7); + this.body.setOffset(this.width * 0.15, this.height * 0.15); this.speed = Phaser.Math.Between(60, 140) * (Math.random() < 0.5 ? 1 : -1); this.startX = x; this.patrolRange = Phaser.Math.Between(80, 200); + } else if (type === 'failed_tx') { + this.setScale(0.55); + this.setTint(0xef4444); + this.body.setSize(this.width * 0.6, this.height * 0.6); + this.body.setOffset(this.width * 0.2, this.height * 0.2); + this.fallSpeed = Phaser.Math.Between(80, 140); + this.driftAmplitude = Phaser.Math.Between(20, 60); + this.driftFreq = Phaser.Math.FloatBetween(0.001, 0.003); + this.spawnTime = scene.time.now; + this.spawnX = x; } } preUpdate(time, delta) { 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; + } else if (this.enemyType === 'failed_tx') { + const dt = delta / 1000; + this.y += this.fallSpeed * dt; + const t = time - this.spawnTime; + this.x = this.spawnX + Math.sin(t * this.driftFreq) * this.driftAmplitude; + this.x = Phaser.Math.Clamp(this.x, 30, GAME_WIDTH - 30); + this.setAngle(Math.sin(t * 0.005) * 15); } } } diff --git a/src/managers/AchievementsManager.js b/src/managers/AchievementsManager.js new file mode 100644 index 0000000..e1ae5bb --- /dev/null +++ b/src/managers/AchievementsManager.js @@ -0,0 +1,149 @@ +import { sound } from './SoundManager.js'; + +const STORAGE_KEY = 'naddie_achievements_v1'; + +const DEFS = [ + { id: 'genesis_block', title: 'Genesis Block', desc: 'Land on a gold platform' }, + { id: 'chain_reaction', title: 'Chain Reaction', desc: 'Reach combo x3.0' }, + { id: 'bug_hunter', title: 'Bug Hunter', desc: 'Stomp 5 enemies in one run' }, + { id: 'first_flight', title: 'First Flight', desc: 'Use the propeller' }, + { id: 'liftoff', title: 'Liftoff', desc: 'Use the rocket' }, + { id: 'power_trip', title: 'Power Trip', desc: 'Use all 3 power-ups in one run' }, + { id: 'survivor', title: 'Survivor', desc: 'Reach block height 500' }, + { id: 'skyscraper', title: 'Skyscraper', desc: 'Reach block height 1000' }, + { id: 'speedrun', title: 'Speedrun', desc: 'Reach block 100 in under 60s' }, + { id: 'gas_baron', title: 'Gas Baron', desc: 'Score 50,000 in one run' }, +]; + +export class AchievementsManager { + constructor(scene) { + this.scene = scene; + this.unlocked = this._load(); + this.popupQueue = []; + this.popupActive = false; + + // Per-run counters + this.bugsStomped = 0; + this.powerupsUsed = new Set(); + this.startTime = Date.now(); + } + + static getAll() { + return DEFS; + } + + _load() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + return raw ? JSON.parse(raw) : {}; + } catch (_) { + return {}; + } + } + + _save() { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(this.unlocked)); + } catch (_) {} + } + + isUnlocked(id) { + return !!this.unlocked[id]; + } + + unlock(id) { + if (this.unlocked[id]) return; + const def = DEFS.find((d) => d.id === id); + if (!def) return; + this.unlocked[id] = Date.now(); + this._save(); + this.popupQueue.push(def); + this._processQueue(); + sound.powerup(); + } + + _processQueue() { + if (this.popupActive || this.popupQueue.length === 0) return; + this.popupActive = true; + const def = this.popupQueue.shift(); + this._showPopup(def, () => { + this.popupActive = false; + this._processQueue(); + }); + } + + _showPopup(def, onDone) { + const { width } = this.scene.scale; + const y = 110; + const panel = this.scene.add.rectangle(width / 2, y, 320, 70, 0x1a0533, 0.92) + .setStrokeStyle(2, 0xffd700) + .setScrollFactor(0) + .setDepth(800); + const title = this.scene.add.text(width / 2, y - 14, `ACHIEVEMENT: ${def.title}`, { + fontFamily: '"Press Start 2P", monospace', + fontSize: '10px', + color: '#ffd700', + }).setOrigin(0.5).setScrollFactor(0).setDepth(801); + const desc = this.scene.add.text(width / 2, y + 10, def.desc, { + fontFamily: '"Press Start 2P", monospace', + fontSize: '8px', + color: '#d8b4fe', + }).setOrigin(0.5).setScrollFactor(0).setDepth(801); + + panel.setAlpha(0); + title.setAlpha(0); + desc.setAlpha(0); + + this.scene.tweens.add({ + targets: [panel, title, desc], + alpha: 1, + y: `+=10`, + duration: 250, + ease: 'Quad.easeOut', + }); + + this.scene.time.delayedCall(2500, () => { + this.scene.tweens.add({ + targets: [panel, title, desc], + alpha: 0, + duration: 350, + onComplete: () => { + panel.destroy(); title.destroy(); desc.destroy(); + onDone(); + }, + }); + }); + } + + // --- Game event hooks --- + + onPlatformLand(type, combo) { + if (type === 'genesis') this.unlock('genesis_block'); + if (combo >= 3) this.unlock('chain_reaction'); + } + + onEnemyStomp() { + this.bugsStomped += 1; + if (this.bugsStomped >= 5) this.unlock('bug_hunter'); + } + + onPowerup(name) { + this.powerupsUsed.add(name); + if (name === 'propeller') this.unlock('first_flight'); + if (name === 'rocket') this.unlock('liftoff'); + if (this.powerupsUsed.size >= 3) this.unlock('power_trip'); + } + + onBlockHeight(blocks) { + if (blocks >= 500) this.unlock('survivor'); + if (blocks >= 1000) this.unlock('skyscraper'); + if (blocks >= 100) { + const elapsed = (Date.now() - this.startTime) / 1000; + if (elapsed <= 60) this.unlock('speedrun'); + } + } + + onScore(score) { + if (score >= 50000) this.unlock('gas_baron'); + } +} diff --git a/src/managers/PlatformManager.js b/src/managers/PlatformManager.js index ea07803..7e129d2 100644 --- a/src/managers/PlatformManager.js +++ b/src/managers/PlatformManager.js @@ -92,8 +92,12 @@ export class PlatformManager { Math.floor(difficultyLevel / 1000) * DIFFICULTY.enemyIncreasePer1000, DIFFICULTY.maxEnemyRate ); - const bugRate = Math.min(ENEMY_RATES.bug + enemyBonus, DIFFICULTY.maxEnemyRate); - if (Math.random() >= bugRate) return; + const totalRate = Math.min(ENEMY_RATES.bug + enemyBonus, DIFFICULTY.maxEnemyRate); + if (Math.random() >= totalRate) return; + + // Failed-Tx type unlocks at higher difficulty + const failedTxAllowed = difficultyLevel > 800; + const type = failedTxAllowed && Math.random() < 0.30 ? 'failed_tx' : 'bug'; const offset = Phaser.Math.Between(-60, 60); const ex = Phaser.Math.Clamp(platformX + offset, 50, GAME_WIDTH - 50); @@ -108,7 +112,7 @@ export class PlatformManager { }); if (tooClose) return; - this.enemies.add(new Enemy(this.scene, ex, ey, 'bug')); + this.enemies.add(new Enemy(this.scene, ex, ey, type)); } getPlatforms() { diff --git a/src/managers/ScoreManager.js b/src/managers/ScoreManager.js index 92a55dd..9e1edae 100644 --- a/src/managers/ScoreManager.js +++ b/src/managers/ScoreManager.js @@ -39,6 +39,18 @@ export class ScoreManager { color: '#aaa', }).setOrigin(1, 0).setScrollFactor(0).setDepth(200).setShadow(1, 1, '#000000', 2, false, true); + // Powerup duration bar (hidden until active) + this.powerupBarBg = scene.add.rectangle(scene.scale.width / 2, scene.scale.height - 24, 200, 12, 0x000000, 0.6) + .setStrokeStyle(2, 0xa855f7) + .setScrollFactor(0).setDepth(200).setVisible(false); + this.powerupBarFill = scene.add.rectangle(scene.scale.width / 2 - 99, scene.scale.height - 24, 196, 8, 0xa855f7) + .setOrigin(0, 0.5).setScrollFactor(0).setDepth(201).setVisible(false); + this.powerupBarLabel = scene.add.text(scene.scale.width / 2, scene.scale.height - 40, '', { + fontFamily: '"Press Start 2P", monospace', + fontSize: '9px', + color: '#d8b4fe', + }).setOrigin(0.5).setScrollFactor(0).setDepth(201).setVisible(false); + this.updateBestDisplay(); } @@ -47,11 +59,51 @@ export class ScoreManager { if (blocks > this.blockHeight) { this.blockHeight = blocks; this.hudBlocks.setText(`Block: ${this.blockHeight}`); + if (this.scene.achievements) this.scene.achievements.onBlockHeight(this.blockHeight); if (this.blockHeight % 100 === 0 && this.blockHeight > 0) { this.showMilestone(this.blockHeight); } } + + this._updatePowerupBar(); + } + + _updatePowerupBar() { + const player = this.scene.player; + if (!player) return; + + let active = null; + let remaining = 0; + let total = 0; + let color = 0xa855f7; + let label = ''; + + if (player.state === 'propeller') { + active = 'propeller'; + remaining = Math.max(0, player.propellerTimer); + total = 3500; + color = 0x44aaff; + label = 'PROPELLER'; + } else if (player.state === 'rocket') { + active = 'rocket'; + remaining = Math.max(0, player.rocketTimer); + total = 3000; + color = 0xff4444; + label = 'ROCKET'; + } + + if (active) { + const pct = remaining / total; + this.powerupBarBg.setVisible(true); + this.powerupBarFill.setVisible(true).setFillStyle(color); + this.powerupBarFill.scaleX = pct; + this.powerupBarLabel.setVisible(true).setText(label); + } else { + this.powerupBarBg.setVisible(false); + this.powerupBarFill.setVisible(false); + this.powerupBarLabel.setVisible(false); + } } onLand(platformY, platformType) { @@ -72,7 +124,13 @@ export class ScoreManager { if (this.genesisJumps <= 0) this.genesisActive = false; } - this.addPoints(Math.floor(basePoints * multiplier)); + const gained = Math.floor(basePoints * multiplier); + this.addPoints(gained); + this._spawnScorePopup(gained, platformType === 'genesis'); + + if (this.scene.achievements) { + this.scene.achievements.onPlatformLand(platformType, this.comboMultiplier); + } if (this.combo > 1) { this.hudCombo.setText(`Combo x${this.comboMultiplier.toFixed(1)}`); @@ -80,15 +138,36 @@ export class ScoreManager { } } + _spawnScorePopup(amount, big) { + const player = this.scene.player; + if (!player) return; + const txt = this.scene.add.text(player.x, player.y - 30, `+${amount}`, { + fontFamily: '"Press Start 2P", monospace', + fontSize: big ? '14px' : '10px', + color: big ? '#ffd700' : '#22c55e', + }).setOrigin(0.5).setDepth(150); + this.scene.tweens.add({ + targets: txt, + y: txt.y - 35, + alpha: 0, + duration: 700, + ease: 'Quad.easeOut', + onComplete: () => txt.destroy(), + }); + } + addPoints(amount) { this.score += amount; this.hudScore.setText(`Gas: ${this.score}`); + if (this.scene.achievements) this.scene.achievements.onScore(this.score); } onFall() { - this.combo = 0; - this.comboMultiplier = 1; - this.hudCombo.setText(''); + if (this.combo > 0) { + this.combo = 0; + this.comboMultiplier = 1; + this.hudCombo.setText(''); + } } showMilestone(blocks) { @@ -133,5 +212,8 @@ export class ScoreManager { this.hudBlocks.destroy(); this.hudCombo.destroy(); this.hudBest.destroy(); + this.powerupBarBg.destroy(); + this.powerupBarFill.destroy(); + this.powerupBarLabel.destroy(); } } diff --git a/src/scenes/GameScene.js b/src/scenes/GameScene.js index 5e07dea..c7809dc 100644 --- a/src/scenes/GameScene.js +++ b/src/scenes/GameScene.js @@ -3,6 +3,7 @@ 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 { AchievementsManager } from '../managers/AchievementsManager.js'; import { sound } from '../managers/SoundManager.js'; import { GAME_WIDTH, GAME_HEIGHT, SCORE, PHYSICS } from '../config/game.config.js'; @@ -19,6 +20,10 @@ export class GameScene extends Scene { .setScrollFactor(0) .setDepth(-10); + // Color overlay for height-based tint shift + this.bgTint = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000033, 0) + .setScrollFactor(0).setDepth(-9); + this.createGweiParticles(); this.cursors = this.input.keyboard.createCursorKeys(); @@ -42,6 +47,7 @@ export class GameScene extends Scene { this.trailGraphics = this.add.graphics().setDepth(-3); this.platformManager = new PlatformManager(this); + this.achievements = new AchievementsManager(this); this.scoreManager = new ScoreManager(this); for (let i = 0; i < 10; i++) { @@ -84,6 +90,10 @@ export class GameScene extends Scene { const height = Math.max(0, GAME_HEIGHT - this.player.y); this.difficultyLevel = height; + + // Background darkens as player climbs higher (cosmic feel) + const tintAlpha = Math.min(0.5, height / 8000); + this.bgTint.fillAlpha = tintAlpha; this.platformManager.update(this.difficultyLevel, killLine); this.scoreManager.update(this.player.y); @@ -160,13 +170,18 @@ export class GameScene extends Scene { if (powerup && typeof powerup.onPlayerTouch === 'function') { const px = powerup.x; const py = powerup.y; - const isSpring = powerup.constructor.name === 'Spring'; + const name = powerup.constructor.name.toLowerCase(); + const isSpring = name === 'spring'; const consumed = powerup.onPlayerTouch(player); if (consumed) { this.createPowerupParticles(px, py); this.flashScreen(); if (isSpring) sound.spring(); else sound.powerup(); + if (this.achievements) { + const map = { spring: 'spring', propellerhat: 'propeller', rocket: 'rocket' }; + this.achievements.onPowerup(map[name] || name); + } } } } @@ -192,6 +207,7 @@ export class GameScene extends Scene { player.jump(); sound.stomp(); this.scoreManager.addPoints(SCORE.stompBonus); + if (this.achievements) this.achievements.onEnemyStomp(); this.createJumpParticles(player.x, player.y + player.displayHeight / 2 + 3); return; }