Sprint 2: gameplay — achievements, score popups, new enemy, polish

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

View File

@@ -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);
}
}
}