Add sound effects and music to slot machine game with victory animations and stage transitions

This commit is contained in:
Brian Fertig 2026-02-28 15:24:39 -07:00
parent 1ca2579b64
commit c3eba35be7
26 changed files with 87 additions and 1 deletions

BIN
assets/demon_sprites.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
assets/fx/coins.mp3 Normal file

Binary file not shown.

BIN
assets/fx/match1.mp3 Normal file

Binary file not shown.

BIN
assets/fx/match2.mp3 Normal file

Binary file not shown.

BIN
assets/fx/match3.mp3 Normal file

Binary file not shown.

BIN
assets/fx/match4.mp3 Normal file

Binary file not shown.

BIN
assets/fx/match5.mp3 Normal file

Binary file not shown.

BIN
assets/fx/match6.mp3 Normal file

Binary file not shown.

BIN
assets/fx/nomatch1.mp3 Normal file

Binary file not shown.

BIN
assets/fx/nomatch2.mp3 Normal file

Binary file not shown.

BIN
assets/fx/nomatch3.mp3 Normal file

Binary file not shown.

BIN
assets/fx/nomatch4.mp3 Normal file

Binary file not shown.

BIN
assets/fx/nomatch5.mp3 Normal file

Binary file not shown.

BIN
assets/fx/nomatch6.mp3 Normal file

Binary file not shown.

BIN
assets/fx/spin.mp3 Normal file

Binary file not shown.

BIN
assets/fx/stop.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -46,6 +46,10 @@ export class LossAnimation {
// Screen tint to dark red briefly
scene.cameras.main.flash(800, 150, 0, 0, true);
// Play a random no-match voice line
const pick = Phaser.Math.Between(1, 6);
scene.sound.play(`sfx-nomatch-${pick}`);
// Tween devil + money up to sin box
scene.tweens.add({
targets: [devilGfx, moneyText],

View File

@ -64,14 +64,25 @@ export class SlotMachine {
const payout = win ? results[0].payout : 0;
// Loop the spin sound for the duration of the spin
this._spinSound = this.scene.sound.add('sfx-spin', { loop: true });
this._spinSound.play();
let doneCount = 0;
const totalReels = this.reels.length;
const stopDelays = [800, 1300, 1800]; // ms before each reel starts decelerating
this.reels.forEach((reel, i) => {
reel.spin(results[i], stopDelays[i], () => {
// Play a thud for each reel landing
this.scene.sound.play('sfx-stop');
doneCount++;
if (doneCount === totalReels) {
// All reels stopped — kill the spin loop
this._spinSound.stop();
this._spinSound.destroy();
this._spinSound = null;
onComplete({ win, symbols: results, payout });
}
});

View File

@ -3,10 +3,17 @@ import { MatchBanner } from './MatchBanner.js';
export class WinAnimation {
// playerBoxCenter, lordBoxCenter: { x, y } screen positions of fund boxes
play(scene, originX, originY, playerBoxCenter, lordBoxCenter, symbol, onComplete) {
// Play a random match voice line as the banner appears
scene.sound.play(`sfx-match-${Phaser.Math.Between(1, 6)}`);
new MatchBanner().play(scene, symbol.label, () => {
// Flash the screen gold
scene.cameras.main.flash(1200, 255, 215, 0, true);
// Loop coins sound for the duration of the coin animation
const coinsSound = scene.sound.add('sfx-coins', { loop: true });
coinsSound.play();
const totalCoins = 20;
const playerCoins = Math.round(totalCoins * 0.6);
const lordCoins = totalCoins - playerCoins;
@ -165,6 +172,8 @@ export class WinAnimation {
container.destroy();
finished++;
if (finished === totalCoins) {
coinsSound.stop();
coinsSound.destroy();
stopPlayer();
stopLord();
scene.tweens.add({

View File

@ -26,6 +26,22 @@ export default class BootScene extends Phaser.Scene {
// Victory videos
this.load.video('lord-victory', 'assets/video/lord-victory.mp4', true);
this.load.video('sin-victory', 'assets/video/sin-victory.mp4', true);
// Background music
this.load.audio('music-01-02', 'assets/music/stage-01-02.mp3');
this.load.audio('music-03-04', 'assets/music/stage-03-04.mp3');
this.load.audio('music-05-06', 'assets/music/stage-05-06.mp3');
this.load.audio('music-lord-victory', 'assets/music/lord-victory.mp3');
this.load.audio('music-sin-victory', 'assets/music/sin-victory.mp3');
// Sound effects
this.load.audio('sfx-spin', 'assets/fx/spin.mp3');
this.load.audio('sfx-stop', 'assets/fx/stop.mp3');
for (let i = 1; i <= 6; i++) {
this.load.audio(`sfx-nomatch-${i}`, `assets/fx/nomatch${i}.mp3`);
this.load.audio(`sfx-match-${i}`, `assets/fx/match${i}.mp3`);
}
this.load.audio('sfx-coins', 'assets/fx/coins.mp3');
}
create() {

View File

@ -23,6 +23,8 @@ export default class GameScene extends Phaser.Scene {
this._currentStage = 2;
this._activeVideo = null;
this._gameOver = false;
this._currentMusicKey = null;
this._musicSound = null;
const vidX = this._vidX;
const vidY = this._vidY;
const vidW = this._vidW;
@ -36,6 +38,7 @@ export default class GameScene extends Phaser.Scene {
// Start with the stage-02 loop (game begins at stage 2)
this._setVideo('stage-02', true);
this._switchMusic(this._getMusicKey(2));
// Gradient backdrop behind title — opaque on left, fades to transparent
const titleBg = this.add.graphics();
@ -101,13 +104,25 @@ export default class GameScene extends Phaser.Scene {
if (!this._gameOver) this.slotMachine.setEnabled(true);
}, this);
// Victory: play the victory video and shatter the losing vial
// Victory: play the victory video, music, and shatter the losing vial
this.game.events.on('vial-winner', ({ winner }) => {
this._gameOver = true;
const lordWins = winner.toLowerCase().includes('lord');
const key = lordWins ? 'lord-victory' : 'sin-victory';
this._setVideo(key, false);
// Fade out background music and play the victory track once
if (this._musicSound) {
const outgoing = this._musicSound;
this._musicSound = null;
this._currentMusicKey = null;
this.tweens.add({
targets: outgoing, volume: 0, duration: 1000,
onComplete: () => { outgoing.stop(); outgoing.destroy(); },
});
}
this.sound.play(lordWins ? 'music-lord-victory' : 'music-sin-victory');
// Shatter the losing vial after a brief dramatic pause
this.time.delayedCall(550, () => {
if (lordWins) {
@ -250,6 +265,8 @@ export default class GameScene extends Phaser.Scene {
const oldStage = this._currentStage;
this._currentStage = newStage;
this._switchMusic(this._getMusicKey(newStage));
const pad = n => String(n).padStart(2, '0');
const direction = newStage > oldStage ? 1 : -1;
const transitions = [];
@ -261,4 +278,33 @@ export default class GameScene extends Phaser.Scene {
if (!this._gameOver) this._setVideo(`stage-${pad(newStage)}`, true);
});
}
/** Map a stage number to its music track key. */
_getMusicKey(stage) {
if (stage <= 2) return 'music-01-02';
if (stage <= 4) return 'music-03-04';
return 'music-05-06';
}
/** Crossfade from the current music track to newKey (no-op if already playing). */
_switchMusic(newKey) {
if (newKey === this._currentMusicKey) return;
this._currentMusicKey = newKey;
// Fade out and destroy the outgoing track
if (this._musicSound) {
const outgoing = this._musicSound;
this.tweens.add({
targets: outgoing,
volume: 0,
duration: 1000,
onComplete: () => { outgoing.stop(); outgoing.destroy(); },
});
}
// Fade in the incoming track from silence
this._musicSound = this.sound.add(newKey, { loop: true, volume: 0 });
this._musicSound.play();
this.tweens.add({ targets: this._musicSound, volume: 1, duration: 1000 });
}
}