Add audio assets and integrate music/SFX handling

- Added new sound effect and music MP3 files to the project.
- Updated **BootScene** to preload all music tracks and sound effects.
- Implemented music playback:
  * MenuScene now plays looping title music and stops it on shutdown.
  * GameScene now stops any playing music on shutdown and starts level‑specific background music with fallback to the last track.
- Added SFX triggers for player shooting, alien shooting, missile launch, impacts, alien death, and player death.
- Cleaned up formatting and minor code style issues in GameScene (group/array declarations, tweens, UI text creation, level setup loops, etc.).
- Ensured proper cleanup of music objects when scenes change.
This commit is contained in:
Brian Fertig 2026-02-21 12:59:46 -07:00
parent f194886ed9
commit 26a80b6b89
17 changed files with 69 additions and 26 deletions

BIN
js/audio/fx/alien_death.mp3 Normal file

Binary file not shown.

BIN
js/audio/fx/alien_shoot.mp3 Normal file

Binary file not shown.

BIN
js/audio/fx/impact.mp3 Normal file

Binary file not shown.

BIN
js/audio/fx/missle.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
js/audio/music/title.mp3 Normal file

Binary file not shown.

BIN
js/audio/music/track01.mp3 Normal file

Binary file not shown.

BIN
js/audio/music/track02.mp3 Normal file

Binary file not shown.

BIN
js/audio/music/track03.mp3 Normal file

Binary file not shown.

BIN
js/audio/music/track04.mp3 Normal file

Binary file not shown.

BIN
js/audio/music/track05.mp3 Normal file

Binary file not shown.

BIN
js/audio/music/track06.mp3 Normal file

Binary file not shown.

BIN
js/audio/music/track07.mp3 Normal file

Binary file not shown.

View File

@ -7,6 +7,24 @@ export default class BootScene extends Phaser.Scene {
super({ key: 'BootScene' }); super({ key: 'BootScene' });
} }
preload() {
this.load.audio('title', 'js/audio/music/title.mp3');
this.load.audio('track01', 'js/audio/music/track01.mp3');
this.load.audio('track02', 'js/audio/music/track02.mp3');
this.load.audio('track03', 'js/audio/music/track03.mp3');
this.load.audio('track04', 'js/audio/music/track04.mp3');
this.load.audio('track05', 'js/audio/music/track05.mp3');
this.load.audio('track06', 'js/audio/music/track06.mp3');
this.load.audio('track07', 'js/audio/music/track07.mp3');
this.load.audio('sfx_player_shoot', 'js/audio/fx/player_shoot.mp3');
this.load.audio('sfx_player_death', 'js/audio/fx/player_death.mp3');
this.load.audio('sfx_alien_shoot', 'js/audio/fx/alien_shoot.mp3');
this.load.audio('sfx_missle', 'js/audio/fx/missle.mp3');
this.load.audio('sfx_impact', 'js/audio/fx/impact.mp3');
this.load.audio('sfx_alien_death', 'js/audio/fx/alien_death.mp3');
}
create() { create() {
this._createTextures(); this._createTextures();
this.scene.start('MenuScene'); this.scene.start('MenuScene');

View File

@ -20,16 +20,16 @@ export default class GameScene extends Phaser.Scene {
this.levelSpeedMult = 1; this.levelSpeedMult = 1;
// Physics groups (used for overlap detection) // Physics groups (used for overlap detection)
this.asteroidsGroup = this.physics.add.group(); this.asteroidsGroup = this.physics.add.group();
this.aliensGroup = this.physics.add.group(); this.aliensGroup = this.physics.add.group();
this.playerBulletsGroup = this.physics.add.group(); this.playerBulletsGroup = this.physics.add.group();
this.alienBulletsGroup = this.physics.add.group(); this.alienBulletsGroup = this.physics.add.group();
// Entity arrays for custom update logic // Entity arrays for custom update logic
this.asteroids = []; this.asteroids = [];
this.aliens = []; this.aliens = [];
this.playerBullets = []; this.playerBullets = [];
this.alienBullets = []; this.alienBullets = [];
this.player = new Player(this, 800, 450); this.player = new Player(this, 800, 450);
@ -49,11 +49,14 @@ export default class GameScene extends Phaser.Scene {
this.cameras.main.setPostPipeline('CRTPipeline'); this.cameras.main.setPostPipeline('CRTPipeline');
} }
// Stop level music when this scene shuts down (e.g. returning to menu)
this.events.once('shutdown', () => { if (this.music) this.music.stop(); });
// Custom reticle cursor – hide the OS cursor and draw our own // Custom reticle cursor – hide the OS cursor and draw our own
this.game.canvas.style.cursor = 'none'; this.game.canvas.style.cursor = 'none';
this.reticleOuter = this.add.image(800, 450, 'reticle_outer').setDepth(20); this.reticleOuter = this.add.image(800, 450, 'reticle_outer').setDepth(20);
this.reticleInner = this.add.image(800, 450, 'reticle_inner').setDepth(20); this.reticleInner = this.add.image(800, 450, 'reticle_inner').setDepth(20);
this.tweens.add({ targets: this.reticleOuter, angle: 360, duration: 8000, repeat: -1, ease: 'Linear' }); this.tweens.add({ targets: this.reticleOuter, angle: 360, duration: 8000, repeat: -1, ease: 'Linear' });
this.tweens.add({ targets: this.reticleInner, angle: -360, duration: 12000, repeat: -1, ease: 'Linear' }); this.tweens.add({ targets: this.reticleInner, angle: -360, duration: 12000, repeat: -1, ease: 'Linear' });
this.events.once('shutdown', () => { this.events.once('shutdown', () => {
this.game.canvas.style.cursor = 'default'; this.game.canvas.style.cursor = 'default';
@ -91,11 +94,11 @@ export default class GameScene extends Phaser.Scene {
createUI() { createUI() {
const mono = { fontFamily: 'monospace', fill: '#ffffff' }; const mono = { fontFamily: 'monospace', fill: '#ffffff' };
this.scoreText = this.add.text(16, 16, 'SCORE: 0', { ...mono, fontSize: '20px' }).setDepth(10); this.scoreText = this.add.text(16, 16, 'SCORE: 0', { ...mono, fontSize: '20px' }).setDepth(10);
this.livesText = this.add.text(16, 44, 'LIVES: 3', { ...mono, fontSize: '20px' }).setDepth(10); this.livesText = this.add.text(16, 44, 'LIVES: 3', { ...mono, fontSize: '20px' }).setDepth(10);
this.levelText = this.add.text(800, 16, 'LEVEL 1', { ...mono, fontSize: '24px' }) this.levelText = this.add.text(800, 16, 'LEVEL 1', { ...mono, fontSize: '24px' })
.setOrigin(0.5, 0).setDepth(10); .setOrigin(0.5, 0).setDepth(10);
this.msgText = this.add.text(800, 420, '', { ...mono, fontSize: '52px', align: 'center' }) this.msgText = this.add.text(800, 420, '', { ...mono, fontSize: '52px', align: 'center' })
.setOrigin(0.5).setDepth(10).setVisible(false); .setOrigin(0.5).setDepth(10).setVisible(false);
} }
@ -115,14 +118,24 @@ export default class GameScene extends Phaser.Scene {
// ─── Level Management ──────────────────────────────────────────────────── // ─── Level Management ────────────────────────────────────────────────────
_playLevelMusic() {
if (this.music) { this.music.stop(); this.music.destroy(); this.music = null; }
const trackNum = String(this.level).padStart(2, '0');
const key = `track${trackNum}`;
// Fall back to the highest available track if this level doesn't have one yet
const resolvedKey = this.cache.audio.exists(key) ? key : 'track07';
this.music = this.sound.add(resolvedKey, { loop: true, volume: 0.7 });
this.music.play();
}
startLevel() { startLevel() {
this.levelComplete = false; this.levelComplete = false;
// Clear all entities // Clear all entities
this.asteroids.forEach(a => a.destroy()); this.asteroids = []; this.asteroids.forEach(a => a.destroy()); this.asteroids = [];
this.aliens.forEach(a => a.destroy()); this.aliens = []; this.aliens.forEach(a => a.destroy()); this.aliens = [];
this.playerBullets.forEach(b => b.destroy()); this.playerBullets = []; this.playerBullets.forEach(b => b.destroy()); this.playerBullets = [];
this.alienBullets.forEach(b => b.destroy()); this.alienBullets = []; this.alienBullets.forEach(b => b.destroy()); this.alienBullets = [];
// Respawn player at center // Respawn player at center
this.player.respawn(800, 450); this.player.respawn(800, 450);
@ -130,20 +143,21 @@ export default class GameScene extends Phaser.Scene {
// Progressive asteroid difficulty: more rocks, mixed sizes, faster per level // Progressive asteroid difficulty: more rocks, mixed sizes, faster per level
this.levelSpeedMult = Math.min(1 + (this.level - 1) * 0.12, 2.0); this.levelSpeedMult = Math.min(1 + (this.level - 1) * 0.12, 2.0);
const largeCount = Math.max(3, 2 + this.level); const largeCount = Math.max(3, 2 + this.level);
const mediumCount = Math.floor(this.level / 2); const mediumCount = Math.floor(this.level / 2);
const smallCount = Math.floor(this.level / 3); const smallCount = Math.floor(this.level / 3);
const spawnOne = (size) => { const spawnOne = (size) => {
const { x, y } = this.edgePosition(); const { x, y } = this.edgePosition();
this.asteroids.push(new Asteroid(this, x, y, size, this.asteroidsGroup, this.levelSpeedMult)); this.asteroids.push(new Asteroid(this, x, y, size, this.asteroidsGroup, this.levelSpeedMult));
}; };
for (let i = 0; i < largeCount; i++) spawnOne('large'); for (let i = 0; i < largeCount; i++) spawnOne('large');
for (let i = 0; i < mediumCount; i++) spawnOne('medium'); for (let i = 0; i < mediumCount; i++) spawnOne('medium');
for (let i = 0; i < smallCount; i++) spawnOne('small'); for (let i = 0; i < smallCount; i++) spawnOne('small');
this.updateUI(); this.updateUI();
this.showMessage(`LEVEL ${this.level}`, 2000); this.showMessage(`LEVEL ${this.level}`, 2000);
this._playLevelMusic();
} }
// Random position along any screen edge, away from center // Random position along any screen edge, away from center
@ -153,10 +167,10 @@ export default class GameScene extends Phaser.Scene {
do { do {
const edge = Phaser.Math.Between(0, 3); const edge = Phaser.Math.Between(0, 3);
switch (edge) { switch (edge) {
case 0: x = Phaser.Math.Between(0, W); y = 0; break; case 0: x = Phaser.Math.Between(0, W); y = 0; break;
case 1: x = Phaser.Math.Between(0, W); y = H; break; case 1: x = Phaser.Math.Between(0, W); y = H; break;
case 2: x = 0; y = Phaser.Math.Between(0, H); break; case 2: x = 0; y = Phaser.Math.Between(0, H); break;
default: x = W; y = Phaser.Math.Between(0, H); break; default: x = W; y = Phaser.Math.Between(0, H); break;
} }
} while (Phaser.Math.Distance.Between(x, y, 800, 450) < 200); } while (Phaser.Math.Distance.Between(x, y, 800, 450) < 200);
return { x, y }; return { x, y };
@ -186,18 +200,21 @@ export default class GameScene extends Phaser.Scene {
} }
spawnPlayerBullet(x, y, angle) { spawnPlayerBullet(x, y, angle) {
this.sound.play('sfx_player_shoot', { volume: 0.2 });
this.playerBullets.push( this.playerBullets.push(
new Bullet(this, x, y, angle, 'player', this.playerBulletsGroup) new Bullet(this, x, y, angle, 'player', this.playerBulletsGroup)
); );
} }
spawnAlienBullet(x, y, angle, ownerShip = null) { spawnAlienBullet(x, y, angle, ownerShip = null) {
this.sound.play('sfx_alien_shoot', { volume: 0.45 });
this.alienBullets.push( this.alienBullets.push(
new Bullet(this, x, y, angle, 'alien', this.alienBulletsGroup, ownerShip) new Bullet(this, x, y, angle, 'alien', this.alienBulletsGroup, ownerShip)
); );
} }
spawnHomingMissile(x, y, ownerShip) { spawnHomingMissile(x, y, ownerShip) {
this.sound.play('sfx_missle', { volume: 0.5 });
const player = this.player; const player = this.player;
const angle = Phaser.Math.RadToDeg( const angle = Phaser.Math.RadToDeg(
Math.atan2(player.sprite.y - y, player.sprite.x - x) Math.atan2(player.sprite.y - y, player.sprite.x - x)
@ -211,7 +228,7 @@ export default class GameScene extends Phaser.Scene {
onPlayerBulletHitAsteroid(bulletSprite, asteroidSprite) { onPlayerBulletHitAsteroid(bulletSprite, asteroidSprite) {
if (!bulletSprite.active || !asteroidSprite.active) return; if (!bulletSprite.active || !asteroidSprite.active) return;
const bullet = bulletSprite.gameEntity; const bullet = bulletSprite.gameEntity;
const asteroid = asteroidSprite.gameEntity; const asteroid = asteroidSprite.gameEntity;
if (!bullet || !asteroid || !bullet.alive || !asteroid.alive) return; if (!bullet || !asteroid || !bullet.alive || !asteroid.alive) return;
@ -228,11 +245,12 @@ export default class GameScene extends Phaser.Scene {
onPlayerBulletHitAlien(bulletSprite, alienSprite) { onPlayerBulletHitAlien(bulletSprite, alienSprite) {
if (!bulletSprite.active || !alienSprite.active) return; if (!bulletSprite.active || !alienSprite.active) return;
const bullet = bulletSprite.gameEntity; const bullet = bulletSprite.gameEntity;
const alien = alienSprite.gameEntity; const alien = alienSprite.gameEntity;
if (!bullet || !alien || !bullet.alive || !alien.alive) return; if (!bullet || !alien || !bullet.alive || !alien.alive) return;
this.addScore(200); this.addScore(200);
this.spawnImpactSparks(bulletSprite.x, bulletSprite.y); this.spawnImpactSparks(bulletSprite.x, bulletSprite.y);
this.sound.play('sfx_alien_death', { volume: 0.65 });
alien.explode(); alien.explode();
this.aliens = this.aliens.filter(a => a !== alien); this.aliens = this.aliens.filter(a => a !== alien);
@ -276,6 +294,7 @@ export default class GameScene extends Phaser.Scene {
handlePlayerHit() { handlePlayerHit() {
if (!this.player.hit()) return; // invincible – ignore if (!this.player.hit()) return; // invincible – ignore
this.sound.play('sfx_player_death', { volume: 0.7 });
this.player.die(); // hide ship and go invincible immediately this.player.die(); // hide ship and go invincible immediately
this.lives--; this.lives--;
this.updateUI(); this.updateUI();
@ -319,6 +338,7 @@ export default class GameScene extends Phaser.Scene {
} }
spawnImpactSparks(x, y) { spawnImpactSparks(x, y) {
this.sound.play('sfx_impact', { volume: 1.0 });
const count = Phaser.Math.Between(3, 5); const count = Phaser.Math.Between(3, 5);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const gfx = this.add.graphics(); const gfx = this.add.graphics();
@ -329,7 +349,7 @@ export default class GameScene extends Phaser.Scene {
gfx.y = y; gfx.y = y;
const angle = Math.random() * Math.PI * 2; const angle = Math.random() * Math.PI * 2;
const dist = Phaser.Math.Between(20, 50); const dist = Phaser.Math.Between(20, 50);
this.tweens.add({ this.tweens.add({
targets: gfx, targets: gfx,
x: x + Math.cos(angle) * dist, x: x + Math.cos(angle) * dist,
@ -408,7 +428,7 @@ export default class GameScene extends Phaser.Scene {
// Update bullets; keep only still-alive ones // Update bullets; keep only still-alive ones
this.playerBullets = this.playerBullets.filter(b => b.update(time)); this.playerBullets = this.playerBullets.filter(b => b.update(time));
this.alienBullets = this.alienBullets.filter(b => b.update(time)); this.alienBullets = this.alienBullets.filter(b => b.update(time));
this.checkLevelComplete(); this.checkLevelComplete();
} }

View File

@ -46,6 +46,11 @@ export default class MenuScene extends Phaser.Scene {
if (this.renderer.type === Phaser.WEBGL) { if (this.renderer.type === Phaser.WEBGL) {
this.cameras.main.setPostPipeline('CRTPipeline'); this.cameras.main.setPostPipeline('CRTPipeline');
} }
// Title music — looping; stop when this scene shuts down
this.music = this.sound.add('title', { loop: true, volume: 0.7 });
this.music.play();
this.events.once('shutdown', () => { if (this.music) this.music.stop(); });
} }
update() { update() {