Boost effects rewrite — flame, wind, speed lines, burst, puff
- EffectsManager handles all powerup visual feedback in one place - Replace circle trail with proper effects per type: * Rocket: yellow->red flame particles from feet + occasional bright spark * Propeller: white/blue wind streaks around player * Spring: gold/green spark trail while ascending fast - Boost start burst: two expanding rings + radial sparks + camera shake (orange/gold for rocket, cyan for propeller, green/gold for spring) - Boost end puff: gray smoke cloud spreading from player - Screen edge speed lines during boost (alternate left/right edges) - Player no longer tilts with horizontal input during boost; rocket stays rigid upright with tiny lean, propeller has gentle sinusoidal wobble
This commit is contained in:
@@ -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 < -this.width / 2) this.x = GAME_WIDTH + this.width / 2;
|
||||||
if (this.x > GAME_WIDTH + this.width / 2) this.x = -this.width / 2;
|
if (this.x > GAME_WIDTH + this.width / 2) this.x = -this.width / 2;
|
||||||
|
|
||||||
if (velocityX < 0) {
|
if (this.state === 'rocket') {
|
||||||
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') {
|
|
||||||
this.rocketTimer -= delta;
|
this.rocketTimer -= delta;
|
||||||
this.setVelocityY(ROCKET_VELOCITY);
|
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();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
238
src/managers/EffectsManager.js
Normal file
238
src/managers/EffectsManager.js
Normal file
@@ -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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { Platform } from '../entities/Platform.js';
|
|||||||
import { PlatformManager } from '../managers/PlatformManager.js';
|
import { PlatformManager } from '../managers/PlatformManager.js';
|
||||||
import { ScoreManager } from '../managers/ScoreManager.js';
|
import { ScoreManager } from '../managers/ScoreManager.js';
|
||||||
import { AchievementsManager } from '../managers/AchievementsManager.js';
|
import { AchievementsManager } from '../managers/AchievementsManager.js';
|
||||||
|
import { EffectsManager } from '../managers/EffectsManager.js';
|
||||||
import { sound } from '../managers/SoundManager.js';
|
import { sound } from '../managers/SoundManager.js';
|
||||||
import { GAME_WIDTH, GAME_HEIGHT, SCORE, PHYSICS } from '../config/game.config.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 = this.add.graphics();
|
||||||
this.playerShadow.setDepth(-4);
|
this.playerShadow.setDepth(-4);
|
||||||
|
|
||||||
this.trailPoints = [];
|
this.effects = new EffectsManager(this);
|
||||||
this.trailGraphics = this.add.graphics().setDepth(-3);
|
|
||||||
|
|
||||||
this.platformManager = new PlatformManager(this);
|
this.platformManager = new PlatformManager(this);
|
||||||
this.achievements = new AchievementsManager(this);
|
this.achievements = new AchievementsManager(this);
|
||||||
@@ -108,7 +108,7 @@ export class GameScene extends Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.updatePlayerShadow();
|
this.updatePlayerShadow();
|
||||||
this.updateTrail();
|
this.effects.update(this.player, delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePlayerShadow() {
|
updatePlayerShadow() {
|
||||||
@@ -119,32 +119,6 @@ export class GameScene extends Scene {
|
|||||||
this.playerShadow.fillCircle(this.player.x, this.player.y + 42, 16);
|
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) {
|
platformCollisionFilter(player, platform) {
|
||||||
if (!platform || !platform.body) return false;
|
if (!platform || !platform.body) return false;
|
||||||
if (typeof platform.isBroken === 'function' && platform.isBroken()) 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 px = powerup.x;
|
||||||
const py = powerup.y;
|
const py = powerup.y;
|
||||||
const name = powerup.constructor.name.toLowerCase();
|
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);
|
const consumed = powerup.onPlayerTouch(player);
|
||||||
if (consumed) {
|
if (consumed) {
|
||||||
this.createPowerupParticles(px, py);
|
this.createPowerupParticles(px, py);
|
||||||
this.flashScreen();
|
this.flashScreen();
|
||||||
if (isSpring) sound.spring();
|
if (kind === 'spring') {
|
||||||
else sound.powerup();
|
sound.spring();
|
||||||
if (this.achievements) {
|
this.effects.startBoost(player, 'spring');
|
||||||
const map = { spring: 'spring', propellerhat: 'propeller', rocket: 'rocket' };
|
} else if (kind === 'propeller' || kind === 'rocket') {
|
||||||
this.achievements.onPowerup(map[name] || name);
|
sound.powerup();
|
||||||
|
this.effects.startBoost(player, kind);
|
||||||
|
} else {
|
||||||
|
sound.powerup();
|
||||||
}
|
}
|
||||||
|
if (this.achievements) this.achievements.onPowerup(kind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user