diff --git a/src/entities/Player.js b/src/entities/Player.js index 81d1414..44e7eee 100644 --- a/src/entities/Player.js +++ b/src/entities/Player.js @@ -34,24 +34,33 @@ export class Player extends Physics.Arcade.Sprite { 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; - if (velocityX < 0) { - this.setFlipX(true); - this.setAngle(-5); - } else if (velocityX > 0) { - this.setFlipX(false); - this.setAngle(5); - } else { - this.setAngle(0); - } - - if (this.state === 'propeller') { - this.propellerTimer -= delta; - this.setVelocityY(PROPELLER_VELOCITY); - if (this.propellerTimer <= 0) this.endPowerUp(); - } else if (this.state === 'rocket') { + if (this.state === 'rocket') { this.rocketTimer -= delta; this.setVelocityY(ROCKET_VELOCITY); + if (velocityX < 0) this.setFlipX(true); + else if (velocityX > 0) this.setFlipX(false); + // Rocket: rigid upright with tiny lean toward movement + this.setAngle(velocityX === 0 ? 0 : (velocityX < 0 ? -3 : 3)); if (this.rocketTimer <= 0) this.endPowerUp(); + } else if (this.state === 'propeller') { + this.propellerTimer -= delta; + this.setVelocityY(PROPELLER_VELOCITY); + if (velocityX < 0) this.setFlipX(true); + else if (velocityX > 0) this.setFlipX(false); + // Propeller: gentle sinusoidal wobble + const wobble = Math.sin(time / 80) * 4; + this.setAngle(wobble + (velocityX < 0 ? -2 : velocityX > 0 ? 2 : 0)); + if (this.propellerTimer <= 0) this.endPowerUp(); + } else { + if (velocityX < 0) { + this.setFlipX(true); + this.setAngle(-5); + } else if (velocityX > 0) { + this.setFlipX(false); + this.setAngle(5); + } else { + this.setAngle(0); + } } } diff --git a/src/managers/EffectsManager.js b/src/managers/EffectsManager.js new file mode 100644 index 0000000..d1d01b7 --- /dev/null +++ b/src/managers/EffectsManager.js @@ -0,0 +1,238 @@ +import { GAME_WIDTH, GAME_HEIGHT } from '../config/game.config.js'; + +const FLAME_COLORS = [0xffd700, 0xffaa00, 0xff6600, 0xff3300, 0xaa0000]; +const WIND_COLORS = [0xffffff, 0xbbddff, 0x66aaff, 0x4477cc]; + +export class EffectsManager { + constructor(scene) { + this.scene = scene; + this.speedLineTimer = 0; + this.lastState = 'normal'; + this.springTrailUntil = 0; + } + + update(player, delta) { + if (!player || !player.active) return; + const state = player.state; + const now = this.scene.time.now; + + if ((state === 'rocket' || state === 'propeller') && Math.abs(player.body.velocity.y) > 150) { + if (state === 'rocket') this._spawnFlame(player); + else this._spawnWind(player); + + this.speedLineTimer += delta; + if (this.speedLineTimer > 40) { + this.speedLineTimer = 0; + this._spawnSpeedLine(); + } + } else if (state === 'normal' && now < this.springTrailUntil && player.body.velocity.y < -500) { + this._spawnSpringSpark(player); + } + + if (this.lastState !== state) { + if ((this.lastState === 'rocket' || this.lastState === 'propeller') && state === 'normal') { + this._spawnEndPuff(player); + } + this.lastState = state; + } + } + + startBoost(player, type) { + this._spawnBurst(player, type); + if (type === 'spring') { + this.springTrailUntil = this.scene.time.now + 700; + } else { + this.lastState = type; + } + } + + _spawnSpringSpark(player) { + const baseY = player.y + player.displayHeight / 2 - 4; + for (let i = 0; i < 2; i++) { + const jitter = Phaser.Math.Between(-12, 12); + const color = Phaser.Utils.Array.GetRandom([0xffd700, 0xfff066, 0x22c55e, 0xa3e635]); + const size = Phaser.Math.Between(4, 9); + const p = this.scene.add.rectangle(player.x + jitter, baseY, size, size, color, 0.9).setDepth(-2); + this.scene.tweens.add({ + targets: p, + x: p.x + Phaser.Math.Between(-25, 25), + y: p.y + Phaser.Math.Between(40, 90), + scaleX: 0, + scaleY: 0, + alpha: 0, + angle: Phaser.Math.Between(-180, 180), + duration: Phaser.Math.Between(400, 600), + ease: 'Quad.easeOut', + onComplete: () => p.destroy(), + }); + } + } + + _spawnFlame(player) { + const baseY = player.y + player.displayHeight / 2 - 6; + const count = 3; + for (let i = 0; i < count; i++) { + const jitter = Phaser.Math.Between(-9, 9); + const color = Phaser.Utils.Array.GetRandom(FLAME_COLORS); + const size = Phaser.Math.Between(8, 16); + const p = this.scene.add.rectangle(player.x + jitter, baseY, size, size, color, 0.95) + .setDepth(-2); + this.scene.tweens.add({ + targets: p, + x: p.x + Phaser.Math.Between(-12, 12), + y: p.y + Phaser.Math.Between(35, 80), + scaleX: 0, + scaleY: 0, + alpha: 0, + angle: Phaser.Math.Between(-180, 180), + duration: Phaser.Math.Between(380, 650), + ease: 'Quad.easeOut', + onComplete: () => p.destroy(), + }); + } + + // Spark + if (Math.random() < 0.4) { + const spark = this.scene.add.circle(player.x + Phaser.Math.Between(-5, 5), baseY, 2, 0xffffaa, 1) + .setDepth(-1); + this.scene.tweens.add({ + targets: spark, + x: spark.x + Phaser.Math.Between(-25, 25), + y: spark.y + Phaser.Math.Between(50, 110), + alpha: 0, + duration: 500, + onComplete: () => spark.destroy(), + }); + } + } + + _spawnWind(player) { + const count = 2; + for (let i = 0; i < count; i++) { + const angle = Phaser.Math.Between(0, 360); + const radius = Phaser.Math.Between(20, 40); + const startX = player.x + Math.cos(angle * Math.PI / 180) * radius; + const startY = player.y + Math.sin(angle * Math.PI / 180) * radius * 0.5; + const color = Phaser.Utils.Array.GetRandom(WIND_COLORS); + const len = Phaser.Math.Between(6, 14); + const p = this.scene.add.rectangle(startX, startY, len, 2, color, 0.75) + .setDepth(-2); + this.scene.tweens.add({ + targets: p, + x: p.x + Phaser.Math.Between(-15, 15), + y: p.y + Phaser.Math.Between(30, 80), + alpha: 0, + scaleX: 0.3, + duration: Phaser.Math.Between(300, 550), + ease: 'Quad.easeOut', + onComplete: () => p.destroy(), + }); + } + } + + _spawnSpeedLine() { + const cam = this.scene.cameras.main; + const side = Math.random() < 0.5 ? 'left' : 'right'; + const x = side === 'left' + ? Phaser.Math.Between(0, 40) + : Phaser.Math.Between(GAME_WIDTH - 40, GAME_WIDTH); + const y = cam.scrollY + Phaser.Math.Between(0, GAME_HEIGHT); + const len = Phaser.Math.Between(30, 70); + const line = this.scene.add.rectangle(x, y, 2, len, 0xffffff, 0.45).setDepth(-1); + this.scene.tweens.add({ + targets: line, + y: line.y + Phaser.Math.Between(150, 280), + alpha: 0, + scaleY: 0.5, + duration: Phaser.Math.Between(280, 450), + ease: 'Quad.easeIn', + onComplete: () => line.destroy(), + }); + } + + _spawnBurst(player, type) { + const cx = player.x; + const cy = player.y; + let colorRing, colorSpark; + if (type === 'rocket') { colorRing = 0xff6600; colorSpark = 0xffd700; } + else if (type === 'spring') { colorRing = 0x22c55e; colorSpark = 0xffd700; } + else { colorRing = 0x88ccff; colorSpark = 0xffffff; } + + // Expanding ring + const ring = this.scene.add.circle(cx, cy, 14, undefined, 0) + .setStrokeStyle(4, colorRing, 0.9) + .setDepth(-1); + this.scene.tweens.add({ + targets: ring, + scale: 6, + alpha: 0, + duration: 380, + ease: 'Quad.easeOut', + onComplete: () => ring.destroy(), + }); + + // Second slower ring + const ring2 = this.scene.add.circle(cx, cy, 10, undefined, 0) + .setStrokeStyle(3, colorSpark, 0.7) + .setDepth(-1); + this.scene.tweens.add({ + targets: ring2, + scale: 4, + alpha: 0, + duration: 520, + ease: 'Quad.easeOut', + onComplete: () => ring2.destroy(), + }); + + // Radial sparks + const sparkCount = type === 'rocket' ? 16 : 10; + for (let i = 0; i < sparkCount; i++) { + const angle = (i / sparkCount) * Math.PI * 2 + Phaser.Math.FloatBetween(-0.1, 0.1); + const dist = Phaser.Math.Between(50, 110); + const p = this.scene.add.rectangle(cx, cy, 5, 5, colorSpark, 1).setDepth(0); + this.scene.tweens.add({ + targets: p, + x: cx + Math.cos(angle) * dist, + y: cy + Math.sin(angle) * dist, + scaleX: 0, + scaleY: 0, + alpha: 0, + duration: 500, + ease: 'Quad.easeOut', + onComplete: () => p.destroy(), + }); + } + + // Brief camera punch + if (type === 'rocket') { + this.scene.cameras.main.shake(120, 0.006); + } else if (type === 'spring') { + this.scene.cameras.main.shake(60, 0.002); + } else { + this.scene.cameras.main.shake(80, 0.003); + } + } + + _spawnEndPuff(player) { + const cx = player.x; + const cy = player.y; + for (let i = 0; i < 8; i++) { + const angle = Phaser.Math.FloatBetween(0, Math.PI * 2); + const dist = Phaser.Math.Between(30, 60); + const size = Phaser.Math.Between(8, 16); + const gray = Phaser.Utils.Array.GetRandom([0x888888, 0xaaaaaa, 0x666666]); + const p = this.scene.add.rectangle(cx, cy, size, size, gray, 0.6).setDepth(-2); + this.scene.tweens.add({ + targets: p, + x: cx + Math.cos(angle) * dist, + y: cy + Math.sin(angle) * dist - 15, + scaleX: 1.5, + scaleY: 1.5, + alpha: 0, + duration: Phaser.Math.Between(500, 800), + ease: 'Quad.easeOut', + onComplete: () => p.destroy(), + }); + } + } +} diff --git a/src/scenes/GameScene.js b/src/scenes/GameScene.js index c7809dc..b1de7aa 100644 --- a/src/scenes/GameScene.js +++ b/src/scenes/GameScene.js @@ -4,6 +4,7 @@ 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 { EffectsManager } from '../managers/EffectsManager.js'; import { sound } from '../managers/SoundManager.js'; import { GAME_WIDTH, GAME_HEIGHT, SCORE, PHYSICS } from '../config/game.config.js'; @@ -43,8 +44,7 @@ export class GameScene extends Scene { this.playerShadow = this.add.graphics(); this.playerShadow.setDepth(-4); - this.trailPoints = []; - this.trailGraphics = this.add.graphics().setDepth(-3); + this.effects = new EffectsManager(this); this.platformManager = new PlatformManager(this); this.achievements = new AchievementsManager(this); @@ -108,7 +108,7 @@ export class GameScene extends Scene { } this.updatePlayerShadow(); - this.updateTrail(); + this.effects.update(this.player, delta); } updatePlayerShadow() { @@ -119,32 +119,6 @@ export class GameScene extends Scene { 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); - } - } - - this.trailGraphics.clear(); - 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; @@ -171,17 +145,22 @@ export class GameScene extends Scene { const px = powerup.x; const py = powerup.y; const name = powerup.constructor.name.toLowerCase(); - const isSpring = name === 'spring'; + const map = { spring: 'spring', propellerhat: 'propeller', rocket: 'rocket' }; + const kind = map[name] || name; 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); + if (kind === 'spring') { + sound.spring(); + this.effects.startBoost(player, 'spring'); + } else if (kind === 'propeller' || kind === 'rocket') { + sound.powerup(); + this.effects.startBoost(player, kind); + } else { + sound.powerup(); } + if (this.achievements) this.achievements.onPowerup(kind); } } }