Add thrust-driven player smoke particles
This commit is contained in:
parent
781b4cbd34
commit
979485e762
@ -34,6 +34,8 @@ const SHOT_LIFETIME = 900;
|
|||||||
const PIRATE_FALLBACK_MAX_HULL = 120;
|
const PIRATE_FALLBACK_MAX_HULL = 120;
|
||||||
const MAX_PIRATES = 6;
|
const MAX_PIRATES = 6;
|
||||||
const HUD_PUBLISH_INTERVAL_MS = 50;
|
const HUD_PUBLISH_INTERVAL_MS = 50;
|
||||||
|
const PLAYER_SMOKE_REAR_OFFSET = 48;
|
||||||
|
const PLAYER_SMOKE_ENGINE_OFFSET = 17;
|
||||||
const AUTO_RESET_MS = 2200;
|
const AUTO_RESET_MS = 2200;
|
||||||
|
|
||||||
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
||||||
@ -62,6 +64,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.helmVector = { x: 0, y: 0 };
|
this.helmVector = { x: 0, y: 0 };
|
||||||
this.pendingPlayerHits = [];
|
this.pendingPlayerHits = [];
|
||||||
this.playerHitSequence = 0;
|
this.playerHitSequence = 0;
|
||||||
|
this.playerSmokeAccumulatorMs = 0;
|
||||||
this.lastDiagnosticsAt = -Infinity;
|
this.lastDiagnosticsAt = -Infinity;
|
||||||
this.lastHudPublishAt = -Infinity;
|
this.lastHudPublishAt = -Infinity;
|
||||||
this.wave = 1;
|
this.wave = 1;
|
||||||
@ -99,6 +102,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.player.body.setAllowGravity(false);
|
this.player.body.setAllowGravity(false);
|
||||||
this.player.body.setCircle(44, 20, 84);
|
this.player.body.setCircle(44, 20, 84);
|
||||||
this.player.setFrame(0);
|
this.player.setFrame(0);
|
||||||
|
this.createPlayerSmoke();
|
||||||
|
|
||||||
this.pirates = this.physics.add.group();
|
this.pirates = this.physics.add.group();
|
||||||
this.playerShots = this.physics.add.group();
|
this.playerShots = this.physics.add.group();
|
||||||
@ -170,6 +174,71 @@ class GameScene extends Phaser.Scene {
|
|||||||
this.spawnWave();
|
this.spawnWave();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
createPlayerSmoke() {
|
||||||
|
if (!this.textures.exists('player-smoke')) {
|
||||||
|
const smokeTexture = this.make.graphics({ x: 0, y: 0, add: false });
|
||||||
|
smokeTexture.fillStyle(0xe3ecee, 0.18);
|
||||||
|
smokeTexture.fillCircle(8, 8, 7);
|
||||||
|
smokeTexture.fillStyle(0xf4f7f7, 0.28);
|
||||||
|
smokeTexture.fillCircle(7, 7, 4.5);
|
||||||
|
smokeTexture.generateTexture('player-smoke', 16, 16);
|
||||||
|
smokeTexture.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playerSmoke = this.add.particles(this.player.x, this.player.y, 'player-smoke', {
|
||||||
|
lifespan: { min: 520, max: 920 },
|
||||||
|
scale: { start: 0.34, end: 1.15 },
|
||||||
|
alpha: { start: 0.24, end: 0 },
|
||||||
|
rotate: { min: 0, max: 360 },
|
||||||
|
frequency: -1,
|
||||||
|
quantity: 1,
|
||||||
|
});
|
||||||
|
this.playerSmoke.setDepth(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlayerSmoke(deltaMs) {
|
||||||
|
if (!this.playerSmoke || !this.player?.active || !this.player.visible) return;
|
||||||
|
|
||||||
|
const forwardThrust = clamp(this.playerSpeed / PLAYER_MAX_SPEED, 0, 1);
|
||||||
|
if (this.gameState !== 'playing' || forwardThrust <= 0.04) {
|
||||||
|
this.playerSmokeAccumulatorMs = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const heading = headingRad(this.player);
|
||||||
|
const forwardX = Math.cos(heading);
|
||||||
|
const forwardY = Math.sin(heading);
|
||||||
|
const sideX = -forwardY;
|
||||||
|
const sideY = forwardX;
|
||||||
|
|
||||||
|
const sternX = this.player.x - forwardX * PLAYER_SMOKE_REAR_OFFSET;
|
||||||
|
const sternY = this.player.y - forwardY * PLAYER_SMOKE_REAR_OFFSET;
|
||||||
|
this.playerSmoke.setPosition(sternX, sternY);
|
||||||
|
|
||||||
|
const emissionIntervalMs = Phaser.Math.Linear(175, 58, forwardThrust);
|
||||||
|
this.playerSmokeAccumulatorMs += deltaMs;
|
||||||
|
|
||||||
|
while (this.playerSmokeAccumulatorMs >= emissionIntervalMs) {
|
||||||
|
const spread = Phaser.Math.FloatBetween(-0.16, 0.16);
|
||||||
|
const trailHeading = heading + Math.PI + spread;
|
||||||
|
const trailSpeed = Phaser.Math.Linear(16, 46, forwardThrust) * Phaser.Math.FloatBetween(0.82, 1.16);
|
||||||
|
this.playerSmoke.setParticleSpeed(
|
||||||
|
Math.cos(trailHeading) * trailSpeed,
|
||||||
|
Math.sin(trailHeading) * trailSpeed,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const engineSide of [-1, 1]) {
|
||||||
|
const jitter = Phaser.Math.FloatBetween(-1.5, 1.5);
|
||||||
|
const engineX = sternX + sideX * (PLAYER_SMOKE_ENGINE_OFFSET * engineSide + jitter);
|
||||||
|
const engineY = sternY + sideY * (PLAYER_SMOKE_ENGINE_OFFSET * engineSide + jitter);
|
||||||
|
this.playerSmoke.emitParticleAt(engineX, engineY, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playerSmokeAccumulatorMs -= emissionIntervalMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createHud() {
|
createHud() {
|
||||||
this.mobileHudActive = !window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
this.mobileHudActive = !window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
||||||
|
|
||||||
@ -401,6 +470,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.updatePlayer(dt, deltaMs);
|
this.updatePlayer(dt, deltaMs);
|
||||||
|
this.updatePlayerSmoke(deltaMs);
|
||||||
this.updatePirates(dt);
|
this.updatePirates(dt);
|
||||||
this.updateProjectiles();
|
this.updateProjectiles();
|
||||||
this.drawWeaponArcs();
|
this.drawWeaponArcs();
|
||||||
@ -927,6 +997,7 @@ class GameScene extends Phaser.Scene {
|
|||||||
}, 'warn');
|
}, 'warn');
|
||||||
|
|
||||||
this.gameState = 'lost';
|
this.gameState = 'lost';
|
||||||
|
this.playerSmokeAccumulatorMs = 0;
|
||||||
this.player.setVelocity(0, 0);
|
this.player.setVelocity(0, 0);
|
||||||
this.playerShots.clear(true, true);
|
this.playerShots.clear(true, true);
|
||||||
this.pirateShots.clear(true, true);
|
this.pirateShots.clear(true, true);
|
||||||
|
|||||||
@ -210,6 +210,23 @@ test('player uses the four-frame north-facing sheet as a forward propulsion anim
|
|||||||
assert.doesNotMatch(source, /key: 'player-idle'/);
|
assert.doesNotMatch(source, /key: 'player-idle'/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('player propulsion smoke is a separate world-space Phaser particle effect', async () => {
|
||||||
|
const source = await read('src/game-v4.js');
|
||||||
|
|
||||||
|
assert.match(source, /this\.createPlayerSmoke\(\)/);
|
||||||
|
assert.match(source, /this\.add\.particles\(this\.player\.x, this\.player\.y, 'player-smoke'/);
|
||||||
|
assert.match(source, /frequency: -1/);
|
||||||
|
assert.match(source, /this\.updatePlayerSmoke\(deltaMs\)/);
|
||||||
|
assert.match(source, /const forwardThrust = clamp\(this\.playerSpeed \/ PLAYER_MAX_SPEED, 0, 1\)/);
|
||||||
|
assert.match(source, /if \(this\.gameState !== 'playing' \|\| forwardThrust <= 0\.04\)/);
|
||||||
|
assert.match(source, /const sternX = this\.player\.x - forwardX \* PLAYER_SMOKE_REAR_OFFSET/);
|
||||||
|
assert.match(source, /const sternY = this\.player\.y - forwardY \* PLAYER_SMOKE_REAR_OFFSET/);
|
||||||
|
assert.match(source, /PLAYER_SMOKE_ENGINE_OFFSET \* engineSide/);
|
||||||
|
assert.match(source, /this\.playerSmoke\.setParticleSpeed\(/);
|
||||||
|
assert.match(source, /this\.playerSmoke\.emitParticleAt\(engineX, engineY, 1\)/);
|
||||||
|
assert.match(source, /Phaser\.Math\.Linear\(175, 58, forwardThrust\)/);
|
||||||
|
});
|
||||||
|
|
||||||
test('mobile HUD skips hidden Phaser rendering and throttles DOM state publishing', async () => {
|
test('mobile HUD skips hidden Phaser rendering and throttles DOM state publishing', async () => {
|
||||||
const source = await read('src/game-v4.js');
|
const source = await read('src/game-v4.js');
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user